Compare commits

..

2 Commits

Author SHA1 Message Date
a0a7eb9100 Fix unreachable code in loop.py and iteration.py
Co-authored-by: JinHai-CN <33142505+JinHai-CN@users.noreply.github.com>
2026-02-06 02:18:09 +00:00
22f17f6334 Initial plan 2026-02-06 02:13:19 +00:00
21 changed files with 229 additions and 416 deletions

View File

@ -48,22 +48,13 @@ RUN --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked \
apt install -y libatk-bridge2.0-0 && \ apt install -y libatk-bridge2.0-0 && \
apt install -y libpython3-dev libgtk-4-1 libnss3 xdg-utils libgbm-dev && \ apt install -y libpython3-dev libgtk-4-1 libnss3 xdg-utils libgbm-dev && \
apt install -y libjemalloc-dev && \ apt install -y libjemalloc-dev && \
apt install -y gnupg unzip curl wget git vim less && \ apt install -y nginx unzip curl wget git vim less && \
apt install -y ghostscript && \ apt install -y ghostscript && \
apt install -y pandoc && \ apt install -y pandoc && \
apt install -y texlive && \ apt install -y texlive && \
apt install -y fonts-freefont-ttf fonts-noto-cjk && \ apt install -y fonts-freefont-ttf fonts-noto-cjk && \
apt install -y postgresql-client apt install -y postgresql-client
ARG NGINX_VERSION=1.29.5-1~noble
RUN --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked \
mkdir -p /etc/apt/keyrings && \
curl -fsSL https://nginx.org/keys/nginx_signing.key | gpg --dearmor -o /etc/apt/keyrings/nginx-archive-keyring.gpg && \
echo "deb [signed-by=/etc/apt/keyrings/nginx-archive-keyring.gpg] https://nginx.org/packages/mainline/ubuntu/ noble nginx" > /etc/apt/sources.list.d/nginx.list && \
apt update && \
apt install -y nginx=${NGINX_VERSION} && \
apt-mark hold nginx
# Install uv # Install uv
RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/,target=/deps \ RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/,target=/deps \
if [ "$NEED_MIRROR" == "1" ]; then \ if [ "$NEED_MIRROR" == "1" ]; then \

View File

@ -57,12 +57,10 @@ class Iteration(ComponentBase, ABC):
return cid return cid
def _invoke(self, **kwargs): def _invoke(self, **kwargs):
if self.check_if_canceled("Iteration processing"): if not self.check_if_canceled("Iteration processing"):
return arr = self._canvas.get_variable_value(self._param.items_ref)
if not isinstance(arr, list):
arr = self._canvas.get_variable_value(self._param.items_ref) self.set_output("_ERROR", self._param.items_ref + " must be an array, but its type is "+str(type(arr)))
if not isinstance(arr, list):
self.set_output("_ERROR", self._param.items_ref + " must be an array, but its type is "+str(type(arr)))
def thoughts(self) -> str: def thoughts(self) -> str:
return "Need to process {} items.".format(len(self._canvas.get_variable_value(self._param.items_ref))) return "Need to process {} items.".format(len(self._canvas.get_variable_value(self._param.items_ref)))

View File

@ -51,29 +51,27 @@ class Loop(ComponentBase, ABC):
return cid return cid
def _invoke(self, **kwargs): def _invoke(self, **kwargs):
if self.check_if_canceled("Loop processing"): if not self.check_if_canceled("Loop processing"):
return for item in self._param.loop_variables:
if any([not item.get("variable"), not item.get("input_mode"), not item.get("value"),not item.get("type")]):
for item in self._param.loop_variables: assert "Loop Variable is not complete."
if any([not item.get("variable"), not item.get("input_mode"), not item.get("value"),not item.get("type")]): if item["input_mode"]=="variable":
assert "Loop Variable is not complete." self.set_output(item["variable"],self._canvas.get_variable_value(item["value"]))
if item["input_mode"]=="variable": elif item["input_mode"]=="constant":
self.set_output(item["variable"],self._canvas.get_variable_value(item["value"])) self.set_output(item["variable"],item["value"])
elif item["input_mode"]=="constant":
self.set_output(item["variable"],item["value"])
else:
if item["type"] == "number":
self.set_output(item["variable"], 0)
elif item["type"] == "string":
self.set_output(item["variable"], "")
elif item["type"] == "boolean":
self.set_output(item["variable"], False)
elif item["type"].startswith("object"):
self.set_output(item["variable"], {})
elif item["type"].startswith("array"):
self.set_output(item["variable"], [])
else: else:
self.set_output(item["variable"], "") if item["type"] == "number":
self.set_output(item["variable"], 0)
elif item["type"] == "string":
self.set_output(item["variable"], "")
elif item["type"] == "boolean":
self.set_output(item["variable"], False)
elif item["type"].startswith("object"):
self.set_output(item["variable"], {})
elif item["type"].startswith("array"):
self.set_output(item["variable"], [])
else:
self.set_output(item["variable"], "")
def thoughts(self) -> str: def thoughts(self) -> str:

View File

@ -617,9 +617,7 @@ async def run():
return get_data_error_result(message="Document not found!") return get_data_error_result(message="Document not found!")
if str(req["run"]) == TaskStatus.CANCEL.value: if str(req["run"]) == TaskStatus.CANCEL.value:
tasks = list(TaskService.query(doc_id=id)) if str(doc.run) == TaskStatus.RUNNING.value:
has_unfinished_task = any((task.progress or 0) < 1 for task in tasks)
if str(doc.run) in [TaskStatus.RUNNING.value, TaskStatus.CANCEL.value] or has_unfinished_task:
cancel_all_task_of(id) cancel_all_task_of(id)
else: else:
return get_data_error_result(message="Cannot cancel a task that is not in RUNNING status") return get_data_error_result(message="Cannot cancel a task that is not in RUNNING status")

View File

@ -204,9 +204,7 @@ class RDBMSConnector(LoadConnector, PollConnector):
value = row_dict[col] value = row_dict[col]
if isinstance(value, (dict, list)): if isinstance(value, (dict, list)):
value = json.dumps(value, ensure_ascii=False) value = json.dumps(value, ensure_ascii=False)
# Use brackets around field name to ensure it's distinguishable content_parts.append(f"{col}: {value}")
# after chunking (TxtParser strips \n delimiters during merge)
content_parts.append(f"{col}】: {value}")
content = "\n".join(content_parts) content = "\n".join(content_parts)

View File

@ -138,24 +138,20 @@ def meta_filter(metas: dict, filters: list[dict], logic: str = "and"):
ids.extend(docids) ids.extend(docids)
return ids return ids
for f in filters: for k, v2docs in metas.items():
k = f["key"] for f in filters:
if k not in metas: if k != f["key"]:
# Key not found in metas: treat as no match continue
ids = []
else:
v2docs = metas[k]
ids = filter_out(v2docs, f["op"], f["value"]) ids = filter_out(v2docs, f["op"], f["value"])
if not doc_ids:
if not doc_ids: doc_ids = set(ids)
doc_ids = set(ids)
else:
if logic == "and":
doc_ids = doc_ids & set(ids)
if not doc_ids:
return []
else: else:
doc_ids = doc_ids | set(ids) if logic == "and":
doc_ids = doc_ids & set(ids)
if not doc_ids:
return []
else:
doc_ids = doc_ids | set(ids)
return list(doc_ids) return list(doc_ids)

View File

@ -156,55 +156,6 @@ class RAGFlowExcelParser:
continue continue
return raw_items return raw_items
@staticmethod
def _get_actual_row_count(ws):
max_row = ws.max_row
if not max_row:
return 0
if max_row <= 10000:
return max_row
max_col = min(ws.max_column or 1, 50)
def row_has_data(row_idx):
for col_idx in range(1, max_col + 1):
cell = ws.cell(row=row_idx, column=col_idx)
if cell.value is not None and str(cell.value).strip():
return True
return False
if not any(row_has_data(i) for i in range(1, min(101, max_row + 1))):
return 0
left, right = 1, max_row
last_data_row = 1
while left <= right:
mid = (left + right) // 2
found = False
for r in range(mid, min(mid + 10, max_row + 1)):
if row_has_data(r):
found = True
last_data_row = max(last_data_row, r)
break
if found:
left = mid + 1
else:
right = mid - 1
for r in range(last_data_row, min(last_data_row + 500, max_row + 1)):
if row_has_data(r):
last_data_row = r
return last_data_row
@staticmethod
def _get_rows_limited(ws):
actual_rows = RAGFlowExcelParser._get_actual_row_count(ws)
if actual_rows == 0:
return []
return list(ws.iter_rows(min_row=1, max_row=actual_rows))
def html(self, fnm, chunk_rows=256): def html(self, fnm, chunk_rows=256):
from html import escape from html import escape
@ -220,7 +171,7 @@ class RAGFlowExcelParser:
for sheetname in wb.sheetnames: for sheetname in wb.sheetnames:
ws = wb[sheetname] ws = wb[sheetname]
try: try:
rows = RAGFlowExcelParser._get_rows_limited(ws) rows = list(ws.rows)
except Exception as e: except Exception as e:
logging.warning(f"Skip sheet '{sheetname}' due to rows access error: {e}") logging.warning(f"Skip sheet '{sheetname}' due to rows access error: {e}")
continue continue
@ -272,7 +223,7 @@ class RAGFlowExcelParser:
for sheetname in wb.sheetnames: for sheetname in wb.sheetnames:
ws = wb[sheetname] ws = wb[sheetname]
try: try:
rows = RAGFlowExcelParser._get_rows_limited(ws) rows = list(ws.rows)
except Exception as e: except Exception as e:
logging.warning(f"Skip sheet '{sheetname}' due to rows access error: {e}") logging.warning(f"Skip sheet '{sheetname}' due to rows access error: {e}")
continue continue
@ -287,8 +238,6 @@ class RAGFlowExcelParser:
t = str(ti[i].value) if i < len(ti) else "" t = str(ti[i].value) if i < len(ti) else ""
t += ("" if t else "") + str(c.value) t += ("" if t else "") + str(c.value)
fields.append(t) fields.append(t)
if not fields:
continue
line = "; ".join(fields) line = "; ".join(fields)
if sheetname.lower().find("sheet") < 0: if sheetname.lower().find("sheet") < 0:
line += " ——" + sheetname line += " ——" + sheetname
@ -300,14 +249,14 @@ class RAGFlowExcelParser:
if fnm.split(".")[-1].lower().find("xls") >= 0: if fnm.split(".")[-1].lower().find("xls") >= 0:
wb = RAGFlowExcelParser._load_excel_to_workbook(BytesIO(binary)) wb = RAGFlowExcelParser._load_excel_to_workbook(BytesIO(binary))
total = 0 total = 0
for sheetname in wb.sheetnames: for sheetname in wb.sheetnames:
try: try:
ws = wb[sheetname] ws = wb[sheetname]
total += RAGFlowExcelParser._get_actual_row_count(ws) total += len(list(ws.rows))
except Exception as e: except Exception as e:
logging.warning(f"Skip sheet '{sheetname}' due to rows access error: {e}") logging.warning(f"Skip sheet '{sheetname}' due to rows access error: {e}")
continue continue
return total return total
if fnm.split(".")[-1].lower() in ["csv", "txt"]: if fnm.split(".")[-1].lower() in ["csv", "txt"]:

View File

@ -31,7 +31,7 @@ At its core, an Agent Context Engine is built on a triumvirate of next-generatio
2. The Memory Layer: An Agents intelligence is defined by its ability to learn from interaction. The Memory Layer is a specialized retrieval system for dynamic, episodic data: conversation history, user preferences, and the agents own internal state (e.g., "waiting for human input"). It manages the lifecycle of this data—storing raw dialogue, triggering summarization into semantic memory, and retrieving relevant past interactions to provide continuity and personalization. Technologically, it is a close sibling to RAG, but focused on a temporal stream of data. 2. The Memory Layer: An Agents intelligence is defined by its ability to learn from interaction. The Memory Layer is a specialized retrieval system for dynamic, episodic data: conversation history, user preferences, and the agents own internal state (e.g., "waiting for human input"). It manages the lifecycle of this data—storing raw dialogue, triggering summarization into semantic memory, and retrieving relevant past interactions to provide continuity and personalization. Technologically, it is a close sibling to RAG, but focused on a temporal stream of data.
3. The Tool Orchestrator: As MCP (Model Context Protocol) enables the connection of hundreds of internal services as tools, a new problem arises: tool selection. The Context Engine solves this with Tool Retrieval. Instead of dumping all tool descriptions into the prompt, it maintains an index of tools and—critically—an index of Skills (best practices on when and how to use tools). For a given task, it retrieves only the most relevant tools and instructions, transforming the LLMs job from "searching a haystack" to "following a recipe." 3. The Tool Orchestrator: As MCP (Model Context Protocol) enables the connection of hundreds of internal services as tools, a new problem arises: tool selection. The Context Engine solves this with Tool Retrieval. Instead of dumping all tool descriptions into the prompt, it maintains an index of tools and—critically—an index of Playbooks or Guidelines (best practices on when and how to use tools). For a given task, it retrieves only the most relevant tools and instructions, transforming the LLMs job from "searching a haystack" to "following a recipe."
## Why we need a dedicated engine? The case for a unified substrate ## Why we need a dedicated engine? The case for a unified substrate

View File

@ -3,7 +3,7 @@ sidebar_position: 1
slug: /what-is-rag slug: /what-is-rag
--- ---
# What is Retrieval-Augmented-Generation (RAG)? # What is Retreival-Augmented-Generation (RAG)?
Since large language models (LLMs) became the focus of technology, their ability to handle general knowledge has been astonishing. However, when questions shift to internal corporate documents, proprietary knowledge bases, or real-time data, the limitations of LLMs become glaringly apparent: they cannot access private information outside their training data. Retrieval-Augmented Generation (RAG) was born precisely to address this core need. Before an LLM generates an answer, it first retrieves the most relevant context from an external knowledge base and inputs it as "reference material" to the LLM, thereby guiding it to produce accurate answers. In short, RAG elevates LLMs from "relying on memory" to "having evidence to rely on," significantly improving their accuracy and trustworthiness in specialized fields and real-time information queries. Since large language models (LLMs) became the focus of technology, their ability to handle general knowledge has been astonishing. However, when questions shift to internal corporate documents, proprietary knowledge bases, or real-time data, the limitations of LLMs become glaringly apparent: they cannot access private information outside their training data. Retrieval-Augmented Generation (RAG) was born precisely to address this core need. Before an LLM generates an answer, it first retrieves the most relevant context from an external knowledge base and inputs it as "reference material" to the LLM, thereby guiding it to produce accurate answers. In short, RAG elevates LLMs from "relying on memory" to "having evidence to rely on," significantly improving their accuracy and trustworthiness in specialized fields and real-time information queries.

View File

@ -19,10 +19,6 @@ from mcp.client.streamable_http import streamablehttp_client
async def main(): async def main():
try: try:
# To access RAGFlow server in `host` mode, you need to attach `api_key` for each request to indicate identification.
# async with streamablehttp_client("http://localhost:9382/mcp/", headers={"api_key": "ragflow-fixS-TicrohljzFkeLLWIaVhW7XlXPXIUW5solFor6o"}) as (read_stream, write_stream, _):
# Or follow the requirements of OAuth 2.1 Section 5 with Authorization header
# async with streamablehttp_client("http://localhost:9382/mcp/", headers={"Authorization": "Bearer ragflow-fixS-TicrohljzFkeLLWIaVhW7XlXPXIUW5solFor6o"}) as (read_stream, write_stream, _):
async with streamablehttp_client("http://localhost:9382/mcp/") as (read_stream, write_stream, _): async with streamablehttp_client("http://localhost:9382/mcp/") as (read_stream, write_stream, _):
async with ClientSession(read_stream, write_stream) as session: async with ClientSession(read_stream, write_stream) as session:
await session.initialize() await session.initialize()

View File

@ -22,18 +22,18 @@ from collections import OrderedDict
from collections.abc import AsyncIterator from collections.abc import AsyncIterator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from functools import wraps from functools import wraps
from typing import Any
import click import click
import httpx import httpx
import mcp.types as types
from mcp.server.lowlevel import Server
from starlette.applications import Starlette from starlette.applications import Starlette
from starlette.middleware import Middleware from starlette.middleware import Middleware
from starlette.responses import JSONResponse, Response from starlette.responses import JSONResponse, Response
from starlette.routing import Mount, Route from starlette.routing import Mount, Route
from strenum import StrEnum from strenum import StrEnum
import mcp.types as types
from mcp.server.lowlevel import Server
class LaunchMode(StrEnum): class LaunchMode(StrEnum):
SELF_HOST = "self-host" SELF_HOST = "self-host"
@ -68,6 +68,10 @@ class RAGFlowConnector:
self.api_url = f"{self.base_url}/api/{self.version}" self.api_url = f"{self.base_url}/api/{self.version}"
self._async_client = None self._async_client = None
def bind_api_key(self, api_key: str):
self.api_key = api_key
self.authorization_header = {"Authorization": f"Bearer {self.api_key}"}
async def _get_client(self): async def _get_client(self):
if self._async_client is None: if self._async_client is None:
self._async_client = httpx.AsyncClient(timeout=httpx.Timeout(60.0)) self._async_client = httpx.AsyncClient(timeout=httpx.Timeout(60.0))
@ -78,18 +82,16 @@ class RAGFlowConnector:
await self._async_client.aclose() await self._async_client.aclose()
self._async_client = None self._async_client = None
async def _post(self, path, json=None, stream=False, files=None, api_key: str = ""): async def _post(self, path, json=None, stream=False, files=None):
if not api_key: if not self.api_key:
return None return None
client = await self._get_client() client = await self._get_client()
res = await client.post(url=self.api_url + path, json=json, headers={"Authorization": f"Bearer {api_key}"}) res = await client.post(url=self.api_url + path, json=json, headers=self.authorization_header)
return res return res
async def _get(self, path, params=None, api_key: str = ""): async def _get(self, path, params=None):
if not api_key:
return None
client = await self._get_client() client = await self._get_client()
res = await client.get(url=self.api_url + path, params=params, headers={"Authorization": f"Bearer {api_key}"}) res = await client.get(url=self.api_url + path, params=params, headers=self.authorization_header)
return res return res
def _is_cache_valid(self, ts): def _is_cache_valid(self, ts):
@ -127,18 +129,8 @@ class RAGFlowConnector:
self._document_metadata_cache[dataset_id] = (doc_id_meta_list, self._get_expiry_timestamp()) self._document_metadata_cache[dataset_id] = (doc_id_meta_list, self._get_expiry_timestamp())
self._document_metadata_cache.move_to_end(dataset_id) self._document_metadata_cache.move_to_end(dataset_id)
async def list_datasets( async def list_datasets(self, page: int = 1, page_size: int = 1000, orderby: str = "create_time", desc: bool = True, id: str | None = None, name: str | None = None):
self, res = await self._get("/datasets", {"page": page, "page_size": page_size, "orderby": orderby, "desc": desc, "id": id, "name": name})
*,
api_key: str,
page: int = 1,
page_size: int = 1000,
orderby: str = "create_time",
desc: bool = True,
id: str | None = None,
name: str | None = None,
):
res = await self._get("/datasets", {"page": page, "page_size": page_size, "orderby": orderby, "desc": desc, "id": id, "name": name}, api_key=api_key)
if not res or res.status_code != 200: if not res or res.status_code != 200:
raise Exception([types.TextContent(type="text", text="Cannot process this operation.")]) raise Exception([types.TextContent(type="text", text="Cannot process this operation.")])
@ -153,8 +145,6 @@ class RAGFlowConnector:
async def retrieval( async def retrieval(
self, self,
*,
api_key: str,
dataset_ids, dataset_ids,
document_ids=None, document_ids=None,
question="", question="",
@ -172,7 +162,7 @@ class RAGFlowConnector:
# If no dataset_ids provided or empty list, get all available dataset IDs # If no dataset_ids provided or empty list, get all available dataset IDs
if not dataset_ids: if not dataset_ids:
dataset_list_str = await self.list_datasets(api_key=api_key) dataset_list_str = await self.list_datasets()
dataset_ids = [] dataset_ids = []
# Parse the dataset list to extract IDs # Parse the dataset list to extract IDs
@ -199,7 +189,7 @@ class RAGFlowConnector:
"document_ids": document_ids, "document_ids": document_ids,
} }
# Send a POST request to the backend service (using requests library as an example, actual implementation may vary) # Send a POST request to the backend service (using requests library as an example, actual implementation may vary)
res = await self._post("/retrieval", json=data_json, api_key=api_key) res = await self._post("/retrieval", json=data_json)
if not res or res.status_code != 200: if not res or res.status_code != 200:
raise Exception([types.TextContent(type="text", text="Cannot process this operation.")]) raise Exception([types.TextContent(type="text", text="Cannot process this operation.")])
@ -209,7 +199,7 @@ class RAGFlowConnector:
chunks = [] chunks = []
# Cache document metadata and dataset information # Cache document metadata and dataset information
document_cache, dataset_cache = await self._get_document_metadata_cache(dataset_ids, api_key=api_key, force_refresh=force_refresh) document_cache, dataset_cache = await self._get_document_metadata_cache(dataset_ids, force_refresh=force_refresh)
# Process chunks with enhanced field mapping including per-chunk metadata # Process chunks with enhanced field mapping including per-chunk metadata
for chunk_data in data.get("chunks", []): for chunk_data in data.get("chunks", []):
@ -238,7 +228,7 @@ class RAGFlowConnector:
raise Exception([types.TextContent(type="text", text=res.get("message"))]) raise Exception([types.TextContent(type="text", text=res.get("message"))])
async def _get_document_metadata_cache(self, dataset_ids, *, api_key: str, force_refresh=False): async def _get_document_metadata_cache(self, dataset_ids, force_refresh=False):
"""Cache document metadata for all documents in the specified datasets""" """Cache document metadata for all documents in the specified datasets"""
document_cache = {} document_cache = {}
dataset_cache = {} dataset_cache = {}
@ -248,7 +238,7 @@ class RAGFlowConnector:
dataset_meta = None if force_refresh else self._get_cached_dataset_metadata(dataset_id) dataset_meta = None if force_refresh else self._get_cached_dataset_metadata(dataset_id)
if not dataset_meta: if not dataset_meta:
# First get dataset info for name # First get dataset info for name
dataset_res = await self._get("/datasets", {"id": dataset_id, "page_size": 1}, api_key=api_key) dataset_res = await self._get("/datasets", {"id": dataset_id, "page_size": 1})
if dataset_res and dataset_res.status_code == 200: if dataset_res and dataset_res.status_code == 200:
dataset_data = dataset_res.json() dataset_data = dataset_res.json()
if dataset_data.get("code") == 0 and dataset_data.get("data"): if dataset_data.get("code") == 0 and dataset_data.get("data"):
@ -265,9 +255,7 @@ class RAGFlowConnector:
doc_id_meta_list = [] doc_id_meta_list = []
docs = {} docs = {}
while page: while page:
docs_res = await self._get(f"/datasets/{dataset_id}/documents?page={page}", api_key=api_key) docs_res = await self._get(f"/datasets/{dataset_id}/documents?page={page}")
if not docs_res:
break
docs_data = docs_res.json() docs_data = docs_res.json()
if docs_data.get("code") == 0 and docs_data.get("data", {}).get("docs"): if docs_data.get("code") == 0 and docs_data.get("data", {}).get("docs"):
for doc in docs_data["data"]["docs"]: for doc in docs_data["data"]["docs"]:
@ -347,59 +335,9 @@ async def sse_lifespan(server: Server) -> AsyncIterator[dict]:
app = Server("ragflow-mcp-server", lifespan=sse_lifespan) app = Server("ragflow-mcp-server", lifespan=sse_lifespan)
AUTH_TOKEN_STATE_KEY = "ragflow_auth_token"
def _to_text(value: Any) -> str: def with_api_key(required=True):
if isinstance(value, bytes):
return value.decode(errors="ignore")
return str(value)
def _extract_token_from_headers(headers: Any) -> str | None:
if not headers or not hasattr(headers, "get"):
return None
auth_keys = ("authorization", "Authorization", b"authorization", b"Authorization")
for key in auth_keys:
auth = headers.get(key)
if not auth:
continue
auth_text = _to_text(auth).strip()
if auth_text.lower().startswith("bearer "):
token = auth_text[7:].strip()
if token:
return token
api_key_keys = ("api_key", "x-api-key", "Api-Key", "X-API-Key", b"api_key", b"x-api-key", b"Api-Key", b"X-API-Key")
for key in api_key_keys:
token = headers.get(key)
if token:
token_text = _to_text(token).strip()
if token_text:
return token_text
return None
def _extract_token_from_request(request: Any) -> str | None:
if request is None:
return None
state = getattr(request, "state", None)
if state is not None:
token = getattr(state, AUTH_TOKEN_STATE_KEY, None)
if token:
return token
token = _extract_token_from_headers(getattr(request, "headers", None))
if token and state is not None:
setattr(state, AUTH_TOKEN_STATE_KEY, token)
return token
def with_api_key(required: bool = True):
def decorator(func): def decorator(func):
@wraps(func) @wraps(func)
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
@ -409,14 +347,26 @@ def with_api_key(required: bool = True):
raise ValueError("Get RAGFlow Context failed") raise ValueError("Get RAGFlow Context failed")
connector = ragflow_ctx.conn connector = ragflow_ctx.conn
api_key = HOST_API_KEY
if MODE == LaunchMode.HOST: if MODE == LaunchMode.HOST:
api_key = _extract_token_from_request(getattr(ctx, "request", None)) or "" headers = ctx.session._init_options.capabilities.experimental.get("headers", {})
if required and not api_key: token = None
# lower case here, because of Starlette conversion
auth = headers.get("authorization", "")
if auth.startswith("Bearer "):
token = auth.removeprefix("Bearer ").strip()
elif "api_key" in headers:
token = headers["api_key"]
if required and not token:
raise ValueError("RAGFlow API key or Bearer token is required.") raise ValueError("RAGFlow API key or Bearer token is required.")
return await func(*args, connector=connector, api_key=api_key, **kwargs) connector.bind_api_key(token)
else:
connector.bind_api_key(HOST_API_KEY)
return await func(*args, connector=connector, **kwargs)
return wrapper return wrapper
@ -425,8 +375,8 @@ def with_api_key(required: bool = True):
@app.list_tools() @app.list_tools()
@with_api_key(required=True) @with_api_key(required=True)
async def list_tools(*, connector: RAGFlowConnector, api_key: str) -> list[types.Tool]: async def list_tools(*, connector) -> list[types.Tool]:
dataset_description = await connector.list_datasets(api_key=api_key) dataset_description = await connector.list_datasets()
return [ return [
types.Tool( types.Tool(
@ -496,13 +446,7 @@ async def list_tools(*, connector: RAGFlowConnector, api_key: str) -> list[types
@app.call_tool() @app.call_tool()
@with_api_key(required=True) @with_api_key(required=True)
async def call_tool( async def call_tool(name: str, arguments: dict, *, connector) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
name: str,
arguments: dict,
*,
connector: RAGFlowConnector,
api_key: str,
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
if name == "ragflow_retrieval": if name == "ragflow_retrieval":
document_ids = arguments.get("document_ids", []) document_ids = arguments.get("document_ids", [])
dataset_ids = arguments.get("dataset_ids", []) dataset_ids = arguments.get("dataset_ids", [])
@ -518,7 +462,7 @@ async def call_tool(
# If no dataset_ids provided or empty list, get all available dataset IDs # If no dataset_ids provided or empty list, get all available dataset IDs
if not dataset_ids: if not dataset_ids:
dataset_list_str = await connector.list_datasets(api_key=api_key) dataset_list_str = await connector.list_datasets()
dataset_ids = [] dataset_ids = []
# Parse the dataset list to extract IDs # Parse the dataset list to extract IDs
@ -533,7 +477,6 @@ async def call_tool(
continue continue
return await connector.retrieval( return await connector.retrieval(
api_key=api_key,
dataset_ids=dataset_ids, dataset_ids=dataset_ids,
document_ids=document_ids, document_ids=document_ids,
question=question, question=question,
@ -567,13 +510,17 @@ def create_starlette_app():
path = scope["path"] path = scope["path"]
if path.startswith("/messages/") or path.startswith("/sse") or path.startswith("/mcp"): if path.startswith("/messages/") or path.startswith("/sse") or path.startswith("/mcp"):
headers = dict(scope["headers"]) headers = dict(scope["headers"])
token = _extract_token_from_headers(headers) token = None
auth_header = headers.get(b"authorization")
if auth_header and auth_header.startswith(b"Bearer "):
token = auth_header.removeprefix(b"Bearer ").strip()
elif b"api_key" in headers:
token = headers[b"api_key"]
if not token: if not token:
response = JSONResponse({"error": "Missing or invalid authorization header"}, status_code=401) response = JSONResponse({"error": "Missing or invalid authorization header"}, status_code=401)
await response(scope, receive, send) await response(scope, receive, send)
return return
scope.setdefault("state", {})[AUTH_TOKEN_STATE_KEY] = token
await self.app(scope, receive, send) await self.app(scope, receive, send)
@ -600,9 +547,10 @@ def create_starlette_app():
# Add streamable HTTP route if enabled # Add streamable HTTP route if enabled
streamablehttp_lifespan = None streamablehttp_lifespan = None
if TRANSPORT_STREAMABLE_HTTP_ENABLED: if TRANSPORT_STREAMABLE_HTTP_ENABLED:
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.types import Receive, Scope, Send from starlette.types import Receive, Scope, Send
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
session_manager = StreamableHTTPSessionManager( session_manager = StreamableHTTPSessionManager(
app=app, app=app,
event_store=None, event_store=None,
@ -610,11 +558,8 @@ def create_starlette_app():
stateless=True, stateless=True,
) )
class StreamableHTTPEntry: async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: await session_manager.handle_request(scope, receive, send)
await session_manager.handle_request(scope, receive, send)
streamable_http_entry = StreamableHTTPEntry()
@asynccontextmanager @asynccontextmanager
async def streamablehttp_lifespan(app: Starlette) -> AsyncIterator[None]: async def streamablehttp_lifespan(app: Starlette) -> AsyncIterator[None]:
@ -625,12 +570,7 @@ def create_starlette_app():
finally: finally:
logging.info("StreamableHTTP application shutting down...") logging.info("StreamableHTTP application shutting down...")
routes.extend( routes.append(Mount("/mcp", app=handle_streamable_http))
[
Route("/mcp", endpoint=streamable_http_entry, methods=["GET", "POST", "DELETE"]),
Mount("/mcp", app=streamable_http_entry),
]
)
return Starlette( return Starlette(
debug=True, debug=True,
@ -691,6 +631,9 @@ def main(base_url, host, port, mode, api_key, transport_sse_enabled, transport_s
if MODE == LaunchMode.SELF_HOST and not HOST_API_KEY: if MODE == LaunchMode.SELF_HOST and not HOST_API_KEY:
raise click.UsageError("--api-key is required when --mode is 'self-host'") raise click.UsageError("--api-key is required when --mode is 'self-host'")
if TRANSPORT_STREAMABLE_HTTP_ENABLED and MODE == LaunchMode.HOST:
raise click.UsageError("The --host mode is not supported with streamable-http transport yet.")
if not TRANSPORT_STREAMABLE_HTTP_ENABLED and JSON_RESPONSE: if not TRANSPORT_STREAMABLE_HTTP_ENABLED and JSON_RESPONSE:
JSON_RESPONSE = False JSON_RESPONSE = False
@ -747,7 +690,7 @@ if __name__ == "__main__":
--base-url=http://127.0.0.1:9380 \ --base-url=http://127.0.0.1:9380 \
--mode=self-host --api-key=ragflow-xxxxx --mode=self-host --api-key=ragflow-xxxxx
2. Host mode (multi-tenant, clients must provide Authorization headers): 2. Host mode (multi-tenant, self-host only, clients must provide Authorization headers):
uv run mcp/server/server.py --host=127.0.0.1 --port=9382 \ uv run mcp/server/server.py --host=127.0.0.1 --port=9382 \
--base-url=http://127.0.0.1:9380 \ --base-url=http://127.0.0.1:9380 \
--mode=host --mode=host

View File

@ -21,7 +21,7 @@ dependencies = [
"cn2an==0.5.22", "cn2an==0.5.22",
"cohere==5.6.2", "cohere==5.6.2",
"Crawl4AI>=0.4.0,<1.0.0", "Crawl4AI>=0.4.0,<1.0.0",
"dashscope==1.25.11", "dashscope==1.20.11",
"deepl==1.18.0", "deepl==1.18.0",
"demjson3==3.0.6", "demjson3==3.0.6",
"discord-py==2.3.2", "discord-py==2.3.2",

View File

@ -44,7 +44,7 @@ class Excel(ExcelParser):
wb = Excel._load_excel_to_workbook(BytesIO(binary)) wb = Excel._load_excel_to_workbook(BytesIO(binary))
total = 0 total = 0
for sheet_name in wb.sheetnames: for sheet_name in wb.sheetnames:
total += Excel._get_actual_row_count(wb[sheet_name]) total += len(list(wb[sheet_name].rows))
res, fails, done = [], [], 0 res, fails, done = [], [], 0
rn = 0 rn = 0
flow_images = [] flow_images = []
@ -66,7 +66,7 @@ class Excel(ExcelParser):
flow_images.append(img) flow_images.append(img)
try: try:
rows = Excel._get_rows_limited(ws) rows = list(ws.rows)
except Exception as e: except Exception as e:
logging.warning(f"Skip sheet '{sheet_name}' due to rows access error: {e}") logging.warning(f"Skip sheet '{sheet_name}' due to rows access error: {e}")
continue continue

View File

@ -165,8 +165,6 @@ def set_progress(task_id, from_page=0, to_page=-1, prog=None, msg="Processing...
if cancel: if cancel:
raise TaskCanceledException(msg) raise TaskCanceledException(msg)
logging.info(f"set_progress({task_id}), progress: {prog}, progress_msg: {msg}") logging.info(f"set_progress({task_id}), progress: {prog}, progress_msg: {msg}")
except TaskCanceledException:
raise
except DoesNotExist: except DoesNotExist:
logging.warning(f"set_progress({task_id}) got exception DoesNotExist") logging.warning(f"set_progress({task_id}) got exception DoesNotExist")
except Exception as e: except Exception as e:
@ -695,8 +693,6 @@ async def run_dataflow(task: dict):
for i, ck in enumerate(chunks): for i, ck in enumerate(chunks):
v = vects[i].tolist() v = vects[i].tolist()
ck["q_%d_vec" % len(v)] = v ck["q_%d_vec" % len(v)] = v
except TaskCanceledException:
raise
except Exception as e: except Exception as e:
set_progress(task_id, prog=-1, msg=f"[ERROR]: {e}") set_progress(task_id, prog=-1, msg=f"[ERROR]: {e}")
PipelineOperationLogService.create(document_id=doc_id, pipeline_id=dataflow_id, PipelineOperationLogService.create(document_id=doc_id, pipeline_id=dataflow_id,
@ -964,9 +960,8 @@ async def do_handle_task(task):
task_tenant_id = task["tenant_id"] task_tenant_id = task["tenant_id"]
task_embedding_id = task["embd_id"] task_embedding_id = task["embd_id"]
task_language = task["language"] task_language = task["language"]
doc_task_llm_id = task["parser_config"].get("llm_id") or task["llm_id"] task_llm_id = task["parser_config"].get("llm_id") or task["llm_id"]
kb_task_llm_id = task['kb_parser_config'].get("llm_id") or task["llm_id"] task["llm_id"] = task_llm_id
task['llm_id'] = kb_task_llm_id
task_dataset_id = task["kb_id"] task_dataset_id = task["kb_id"]
task_doc_id = task["doc_id"] task_doc_id = task["doc_id"]
task_document_name = task["name"] task_document_name = task["name"]
@ -1037,7 +1032,7 @@ async def do_handle_task(task):
return return
# bind LLM for raptor # bind LLM for raptor
chat_model = LLMBundle(task_tenant_id, LLMType.CHAT, llm_name=kb_task_llm_id, lang=task_language) chat_model = LLMBundle(task_tenant_id, LLMType.CHAT, llm_name=task_llm_id, lang=task_language)
# run RAPTOR # run RAPTOR
async with kg_limiter: async with kg_limiter:
chunks, token_count = await run_raptor_for_kb( chunks, token_count = await run_raptor_for_kb(
@ -1081,7 +1076,7 @@ async def do_handle_task(task):
graphrag_conf = kb_parser_config.get("graphrag", {}) graphrag_conf = kb_parser_config.get("graphrag", {})
start_ts = timer() start_ts = timer()
chat_model = LLMBundle(task_tenant_id, LLMType.CHAT, llm_name=kb_task_llm_id, lang=task_language) chat_model = LLMBundle(task_tenant_id, LLMType.CHAT, llm_name=task_llm_id, lang=task_language)
with_resolution = graphrag_conf.get("resolution", False) with_resolution = graphrag_conf.get("resolution", False)
with_community = graphrag_conf.get("community", False) with_community = graphrag_conf.get("community", False)
async with kg_limiter: async with kg_limiter:
@ -1106,7 +1101,6 @@ async def do_handle_task(task):
return return
else: else:
# Standard chunking methods # Standard chunking methods
task['llm_id'] = doc_task_llm_id
start_ts = timer() start_ts = timer()
chunks = await build_chunks(task, progress_callback) chunks = await build_chunks(task, progress_callback)
logging.info("Build document {}: {:.2f}s".format(task_document_name, timer() - start_ts)) logging.info("Build document {}: {:.2f}s".format(task_document_name, timer() - start_ts))
@ -1117,8 +1111,6 @@ async def do_handle_task(task):
start_ts = timer() start_ts = timer()
try: try:
token_count, vector_size = await embedding(chunks, embedding_model, task_parser_config, progress_callback) token_count, vector_size = await embedding(chunks, embedding_model, task_parser_config, progress_callback)
except TaskCanceledException:
raise
except Exception as e: except Exception as e:
error_message = "Generate embedding error:{}".format(str(e)) error_message = "Generate embedding error:{}".format(str(e))
progress_callback(-1, error_message) progress_callback(-1, error_message)
@ -1136,17 +1128,13 @@ async def do_handle_task(task):
async def _maybe_insert_chunks(_chunks): async def _maybe_insert_chunks(_chunks):
if has_canceled(task_id): if has_canceled(task_id):
progress_callback(-1, msg="Task has been canceled.") return True
return False
insert_result = await insert_chunks(task_id, task_tenant_id, task_dataset_id, _chunks, progress_callback) insert_result = await insert_chunks(task_id, task_tenant_id, task_dataset_id, _chunks, progress_callback)
return bool(insert_result) return bool(insert_result)
try: try:
if not await _maybe_insert_chunks(chunks): if not await _maybe_insert_chunks(chunks):
return return
if has_canceled(task_id):
progress_callback(-1, msg="Task has been canceled.")
return
logging.info( logging.info(
"Indexing doc({}), page({}-{}), chunks({}), elapsed: {:.2f}".format( "Indexing doc({}), page({}-{}), chunks({}), elapsed: {:.2f}".format(
@ -1215,12 +1203,6 @@ async def handle_task():
DONE_TASKS += 1 DONE_TASKS += 1
CURRENT_TASKS.pop(task_id, None) CURRENT_TASKS.pop(task_id, None)
logging.info(f"handle_task done for task {json.dumps(task)}") logging.info(f"handle_task done for task {json.dumps(task)}")
except TaskCanceledException as e:
DONE_TASKS += 1
CURRENT_TASKS.pop(task_id, None)
logging.info(
f"handle_task canceled for task {task_id}: {getattr(e, 'msg', str(e))}"
)
except Exception as e: except Exception as e:
FAILED_TASKS += 1 FAILED_TASKS += 1
CURRENT_TASKS.pop(task_id, None) CURRENT_TASKS.pop(task_id, None)

8
uv.lock generated
View File

@ -1557,17 +1557,15 @@ wheels = [
[[package]] [[package]]
name = "dashscope" name = "dashscope"
version = "1.25.11" version = "1.20.11"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [ dependencies = [
{ name = "aiohttp" }, { name = "aiohttp" },
{ name = "certifi" },
{ name = "cryptography" },
{ name = "requests" }, { name = "requests" },
{ name = "websocket-client" }, { name = "websocket-client" },
] ]
wheels = [ wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/15/35551e6c6d3ea19df754ed32aa5f281b2052ef9e1ff1538f2708f74f3312/dashscope-1.25.11-py3-none-any.whl", hash = "sha256:93e86437f5f30e759e98292f0490e44eff00c337968363f27d29dd42ec7cc07c", size = 1342054, upload-time = "2026-02-03T02:49:48.711Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/21/0ddfa1aae7f45b3039d10d61ede77dedfc70d24ff946e7d0ecb92e9a2c85/dashscope-1.20.11-py3-none-any.whl", hash = "sha256:7367802c5ae136c6c1f4f8a16f9aba628e97adefae8afdebce6bbf518d0065d1", size = 1264221, upload-time = "2024-10-14T05:30:25.083Z" },
] ]
[[package]] [[package]]
@ -6273,7 +6271,7 @@ requires-dist = [
{ name = "cn2an", specifier = "==0.5.22" }, { name = "cn2an", specifier = "==0.5.22" },
{ name = "cohere", specifier = "==5.6.2" }, { name = "cohere", specifier = "==5.6.2" },
{ name = "crawl4ai", specifier = ">=0.4.0,<1.0.0" }, { name = "crawl4ai", specifier = ">=0.4.0,<1.0.0" },
{ name = "dashscope", specifier = "==1.25.11" }, { name = "dashscope", specifier = "==1.20.11" },
{ name = "deepl", specifier = "==1.18.0" }, { name = "deepl", specifier = "==1.18.0" },
{ name = "demjson3", specifier = "==3.0.6" }, { name = "demjson3", specifier = "==3.0.6" },
{ name = "discord-py", specifier = "==2.3.2" }, { name = "discord-py", specifier = "==2.3.2" },

View File

@ -73,7 +73,6 @@ if (process.env.NODE_ENV === 'development') {
trackAllPureComponents: true, trackAllPureComponents: true,
trackExtraHooks: [], trackExtraHooks: [],
logOnDifferentValues: true, logOnDifferentValues: true,
exclude: [/^RouterProvider$/],
}); });
}, },
); );
@ -151,13 +150,6 @@ const RootProvider = ({ children }: React.PropsWithChildren) => {
); );
}; };
const RouterProviderWrapper: React.FC<{ router: typeof routers }> = ({
router,
}) => {
return <RouterProvider router={router}></RouterProvider>;
};
RouterProviderWrapper.whyDidYouRender = false;
export default function AppContainer() { export default function AppContainer() {
// const [router, setRouter] = useState<any>(null); // const [router, setRouter] = useState<any>(null);
@ -171,7 +163,8 @@ export default function AppContainer() {
return ( return (
<RootProvider> <RootProvider>
<RouterProviderWrapper router={routers} /> <RouterProvider router={routers}></RouterProvider>
{/* <RouterProvider router={router}></RouterProvider> */}
</RootProvider> </RootProvider>
); );
} }

View File

@ -101,6 +101,7 @@ export const RAGFlowAvatar = memo(
}} }}
className={cn( className={cn(
'bg-gradient-to-b', 'bg-gradient-to-b',
`from-[${from}] to-[${to}]`,
'flex items-center justify-center', 'flex items-center justify-center',
'text-white ', 'text-white ',
{ 'rounded-md': !isPerson }, { 'rounded-md': !isPerson },

View File

@ -4,7 +4,6 @@ import * as DialogPrimitive from '@radix-ui/react-dialog';
import { Loader, X } from 'lucide-react'; import { Loader, X } from 'lucide-react';
import { FC, ReactNode, useCallback, useEffect, useMemo } from 'react'; import { FC, ReactNode, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DialogDescription } from '../dialog';
import { createPortalModal } from './modal-manage'; import { createPortalModal } from './modal-manage';
export interface ModalProps { export interface ModalProps {
@ -185,7 +184,6 @@ const Modal: ModalType = ({
style={style} style={style}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<DialogDescription></DialogDescription>
{/* title */} {/* title */}
{title && ( {title && (
<div <div

View File

@ -3,7 +3,6 @@ import { ButtonLoading } from '@/components/ui/button';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
@ -156,20 +155,10 @@ export function DatasetCreatingDialog({
return ( return (
<Dialog open onOpenChange={hideModal}> <Dialog open onOpenChange={hideModal}>
<DialogContent <DialogContent className="sm:max-w-[425px] focus-visible:!outline-none flex flex-col">
className="sm:max-w-[425px] focus-visible:!outline-none flex flex-col"
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
const form = document.getElementById(FormId) as HTMLFormElement;
form?.requestSubmit();
}
}}
>
<DialogHeader> <DialogHeader>
<DialogTitle>{t('knowledgeList.createKnowledgeBase')}</DialogTitle> <DialogTitle>{t('knowledgeList.createKnowledgeBase')}</DialogTitle>
</DialogHeader> </DialogHeader>
<DialogDescription></DialogDescription>
<InputForm onOk={onOk}></InputForm> <InputForm onOk={onOk}></InputForm>
<DialogFooter> <DialogFooter>
<ButtonLoading type="submit" form={FormId} loading={loading}> <ButtonLoading type="submit" form={FormId} loading={loading}>

View File

@ -1,4 +1,4 @@
import { lazy, Suspense } from 'react'; import { lazy } from 'react';
import { createBrowserRouter, Navigate, type RouteObject } from 'react-router'; import { createBrowserRouter, Navigate, type RouteObject } from 'react-router';
import FallbackComponent from './components/fallback-component'; import FallbackComponent from './components/fallback-component';
import { IS_ENTERPRISE } from './pages/admin/utils'; import { IS_ENTERPRISE } from './pages/admin/utils';
@ -66,253 +66,252 @@ export enum Routes {
AdminMonitoring = `${Admin}/monitoring`, AdminMonitoring = `${Admin}/monitoring`,
} }
const defaultRouteFallback = ( const routeConfig = [
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30 backdrop-blur-[1px]">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-white/70 border-t-transparent" />
</div>
);
type LazyRouteConfig = Omit<RouteObject, 'Component' | 'children'> & {
Component?: () => Promise<{ default: React.ComponentType<any> }>;
children?: LazyRouteConfig[];
};
const withLazyRoute = (
importer: () => Promise<{ default: React.ComponentType<any> }>,
fallback: React.ReactNode = defaultRouteFallback,
) => {
const LazyComponent = lazy(importer);
const Wrapped: React.FC<any> = (props) => (
<Suspense fallback={fallback}>
<LazyComponent {...props} />
</Suspense>
);
Wrapped.displayName = `LazyRoute(${
(LazyComponent as unknown as React.ComponentType<any>).displayName ||
LazyComponent.name ||
'Component'
})`;
return Wrapped;
};
const routeConfigOptions = [
{ {
path: '/login', path: '/login',
Component: () => import('@/pages/login-next'), Component: lazy(() => import('@/pages/login-next')),
layout: false, layout: false,
errorElement: <FallbackComponent />,
}, },
{ {
path: '/login-next', path: '/login-next',
Component: () => import('@/pages/login-next'), Component: lazy(() => import('@/pages/login-next')),
layout: false, layout: false,
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.ChatShare, path: Routes.ChatShare,
Component: () => import('@/pages/next-chats/share'), Component: lazy(() => import('@/pages/next-chats/share')),
layout: false, layout: false,
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.AgentShare, path: Routes.AgentShare,
Component: () => import('@/pages/agent/share'), Component: lazy(() => import('@/pages/agent/share')),
layout: false, layout: false,
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.ChatWidget, path: Routes.ChatWidget,
Component: () => import('@/pages/next-chats/widget'), Component: lazy(() => import('@/pages/next-chats/widget')),
layout: false, layout: false,
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.AgentList, path: Routes.AgentList,
Component: () => import('@/pages/agents'), Component: lazy(() => import('@/pages/agents')),
errorElement: <FallbackComponent />,
}, },
{ {
path: '/document/:id', path: '/document/:id',
Component: () => import('@/pages/document-viewer'), Component: lazy(() => import('@/pages/document-viewer')),
layout: false, layout: false,
errorElement: <FallbackComponent />,
}, },
{ {
path: '/*', path: '/*',
Component: () => import('@/pages/404'), Component: lazy(() => import('@/pages/404')),
layout: false, layout: false,
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.Root, path: Routes.Root,
layout: false, layout: false,
Component: () => import('@/layouts/next'), Component: lazy(() => import('@/layouts/next')),
wrappers: ['@/wrappers/auth'], wrappers: ['@/wrappers/auth'],
children: [ children: [
{ {
path: Routes.Root, path: Routes.Root,
Component: () => import('@/pages/home'), Component: lazy(() => import('@/pages/home')),
}, },
], ],
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.Datasets, path: Routes.Datasets,
layout: false, layout: false,
Component: () => import('@/layouts/next'), Component: lazy(() => import('@/layouts/next')),
children: [ children: [
{ {
path: Routes.Datasets, path: Routes.Datasets,
Component: () => import('@/pages/datasets'), Component: lazy(() => import('@/pages/datasets')),
}, },
], ],
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.Chats, path: Routes.Chats,
layout: false, layout: false,
Component: () => import('@/layouts/next'), Component: lazy(() => import('@/layouts/next')),
children: [ children: [
{ {
path: Routes.Chats, path: Routes.Chats,
Component: () => import('@/pages/next-chats'), Component: lazy(() => import('@/pages/next-chats')),
}, },
], ],
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.Chat + '/:id', path: Routes.Chat + '/:id',
layout: false, layout: false,
Component: () => import('@/pages/next-chats/chat'), Component: lazy(() => import('@/pages/next-chats/chat')),
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.Searches, path: Routes.Searches,
layout: false, layout: false,
Component: () => import('@/layouts/next'), Component: lazy(() => import('@/layouts/next')),
children: [ children: [
{ {
path: Routes.Searches, path: Routes.Searches,
Component: () => import('@/pages/next-searches'), Component: lazy(() => import('@/pages/next-searches')),
}, },
], ],
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.Memories, path: Routes.Memories,
layout: false, layout: false,
Component: () => import('@/layouts/next'), Component: lazy(() => import('@/layouts/next')),
children: [ children: [
{ {
path: Routes.Memories, path: Routes.Memories,
Component: () => import('@/pages/memories'), Component: lazy(() => import('@/pages/memories')),
}, },
], ],
errorElement: <FallbackComponent />,
}, },
{ {
path: `${Routes.Memory}`, path: `${Routes.Memory}`,
layout: false, layout: false,
Component: () => import('@/layouts/next'), Component: lazy(() => import('@/layouts/next')),
children: [ children: [
{ {
path: `${Routes.Memory}`, path: `${Routes.Memory}`,
layout: false, layout: false,
Component: () => import('@/pages/memory'), Component: lazy(() => import('@/pages/memory')),
children: [ children: [
{ {
path: `${Routes.Memory}/${Routes.MemoryMessage}/:id`, path: `${Routes.Memory}/${Routes.MemoryMessage}/:id`,
Component: () => import('@/pages/memory/memory-message'), Component: lazy(() => import('@/pages/memory/memory-message')),
}, },
{ {
path: `${Routes.Memory}/${Routes.MemorySetting}/:id`, path: `${Routes.Memory}/${Routes.MemorySetting}/:id`,
Component: () => import('@/pages/memory/memory-setting'), Component: lazy(() => import('@/pages/memory/memory-setting')),
}, },
], ],
}, },
], ],
errorElement: <FallbackComponent />,
}, },
{ {
path: `${Routes.Search}/:id`, path: `${Routes.Search}/:id`,
layout: false, layout: false,
Component: () => import('@/pages/next-search'), Component: lazy(() => import('@/pages/next-search')),
errorElement: <FallbackComponent />,
}, },
{ {
path: `${Routes.SearchShare}`, path: `${Routes.SearchShare}`,
layout: false, layout: false,
Component: () => import('@/pages/next-search/share'), Component: lazy(() => import('@/pages/next-search/share')),
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.Agents, path: Routes.Agents,
layout: false, layout: false,
Component: () => import('@/layouts/next'), Component: lazy(() => import('@/layouts/next')),
children: [ children: [
{ {
path: Routes.Agents, path: Routes.Agents,
Component: () => import('@/pages/agents'), Component: lazy(() => import('@/pages/agents')),
}, },
], ],
errorElement: <FallbackComponent />,
}, },
{ {
path: `${Routes.AgentLogPage}/:id`, path: `${Routes.AgentLogPage}/:id`,
layout: false, layout: false,
Component: () => import('@/pages/agents/agent-log-page'), Component: lazy(() => import('@/pages/agents/agent-log-page')),
errorElement: <FallbackComponent />,
}, },
{ {
path: `${Routes.Agent}/:id`, path: `${Routes.Agent}/:id`,
layout: false, layout: false,
Component: () => import('@/pages/agent'), Component: lazy(() => import('@/pages/agent')),
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.AgentTemplates, path: Routes.AgentTemplates,
layout: false, layout: false,
Component: () => import('@/pages/agents/agent-templates'), Component: lazy(() => import('@/pages/agents/agent-templates')),
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.Files, path: Routes.Files,
layout: false, layout: false,
Component: () => import('@/layouts/next'), Component: lazy(() => import('@/layouts/next')),
children: [ children: [
{ {
path: Routes.Files, path: Routes.Files,
Component: () => import('@/pages/files'), Component: lazy(() => import('@/pages/files')),
}, },
], ],
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.DatasetBase, path: Routes.DatasetBase,
layout: false, layout: false,
Component: () => import('@/layouts/next'), Component: lazy(() => import('@/layouts/next')),
children: [ children: [
{ {
path: Routes.DatasetBase, path: Routes.DatasetBase,
element: <Navigate to={Routes.Dataset} replace />, element: <Navigate to={Routes.Dataset} replace />,
}, },
], ],
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.DatasetBase, path: Routes.DatasetBase,
layout: false, layout: false,
Component: () => import('@/pages/dataset'), Component: lazy(() => import('@/pages/dataset')),
children: [ children: [
{ {
path: `${Routes.Dataset}/:id`, path: `${Routes.Dataset}/:id`,
Component: () => import('@/pages/dataset/dataset'), Component: lazy(() => import('@/pages/dataset/dataset')),
}, },
{ {
path: `${Routes.DatasetBase}${Routes.DatasetTesting}/:id`, path: `${Routes.DatasetBase}${Routes.DatasetTesting}/:id`,
Component: () => import('@/pages/dataset/testing'), Component: lazy(() => import('@/pages/dataset/testing')),
}, },
{ {
path: `${Routes.DatasetBase}${Routes.KnowledgeGraph}/:id`, path: `${Routes.DatasetBase}${Routes.KnowledgeGraph}/:id`,
Component: () => import('@/pages/dataset/knowledge-graph'), Component: lazy(() => import('@/pages/dataset/knowledge-graph')),
}, },
{ {
path: `${Routes.DatasetBase}${Routes.DataSetOverview}/:id`, path: `${Routes.DatasetBase}${Routes.DataSetOverview}/:id`,
Component: () => import('@/pages/dataset/dataset-overview'), Component: lazy(() => import('@/pages/dataset/dataset-overview')),
}, },
{ {
path: `${Routes.DatasetBase}${Routes.DataSetSetting}/:id`, path: `${Routes.DatasetBase}${Routes.DataSetSetting}/:id`,
Component: () => import('@/pages/dataset/dataset-setting'), Component: lazy(() => import('@/pages/dataset/dataset-setting')),
}, },
], ],
errorElement: <FallbackComponent />,
}, },
{ {
path: `${Routes.DataflowResult}`, path: `${Routes.DataflowResult}`,
layout: false, layout: false,
Component: () => import('@/pages/dataflow-result'), Component: lazy(() => import('@/pages/dataflow-result')),
errorElement: <FallbackComponent />,
}, },
{ {
path: `${Routes.ParsedResult}/chunks`, path: `${Routes.ParsedResult}/chunks`,
layout: false, layout: false,
Component: () => Component: lazy(
import('@/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk'), () =>
import('@/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk'),
),
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.Chunk, path: Routes.Chunk,
@ -320,28 +319,30 @@ const routeConfigOptions = [
children: [ children: [
{ {
path: Routes.Chunk, path: Routes.Chunk,
Component: () => import('@/pages/chunk'), Component: lazy(() => import('@/pages/chunk')),
children: [ children: [
{ {
path: `${Routes.ChunkResult}/:id`, path: `${Routes.ChunkResult}/:id`,
Component: () => import('@/pages/chunk/chunk-result'), Component: lazy(() => import('@/pages/chunk/chunk-result')),
}, },
{ {
path: `${Routes.ResultView}/:id`, path: `${Routes.ResultView}/:id`,
Component: () => import('@/pages/chunk/result-view'), Component: lazy(() => import('@/pages/chunk/result-view')),
}, },
], ],
}, },
], ],
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.Chunk, path: Routes.Chunk,
layout: false, layout: false,
Component: () => import('@/pages/chunk'), Component: lazy(() => import('@/pages/chunk')),
errorElement: <FallbackComponent />,
}, },
{ {
path: '/user-setting', path: '/user-setting',
Component: () => import('@/pages/user-setting'), Component: lazy(() => import('@/pages/user-setting')),
layout: false, layout: false,
children: [ children: [
{ {
@ -350,87 +351,92 @@ const routeConfigOptions = [
}, },
{ {
path: '/user-setting/profile', path: '/user-setting/profile',
Component: () => import('@/pages/user-setting/profile'), Component: lazy(() => import('@/pages/user-setting/profile')),
}, },
{ {
path: '/user-setting/locale', path: '/user-setting/locale',
Component: () => import('@/pages/user-setting/setting-locale'), Component: lazy(() => import('@/pages/user-setting/setting-locale')),
}, },
{ {
path: '/user-setting/model', path: '/user-setting/model',
Component: () => import('@/pages/user-setting/setting-model'), Component: lazy(() => import('@/pages/user-setting/setting-model')),
}, },
{ {
path: '/user-setting/team', path: '/user-setting/team',
Component: () => import('@/pages/user-setting/setting-team'), Component: lazy(() => import('@/pages/user-setting/setting-team')),
}, },
{ {
path: `/user-setting${Routes.Api}`, path: `/user-setting${Routes.Api}`,
Component: () => import('@/pages/user-setting/setting-api'), Component: lazy(() => import('@/pages/user-setting/setting-api')),
}, },
{ {
path: `/user-setting${Routes.Mcp}`, path: `/user-setting${Routes.Mcp}`,
Component: () => import('@/pages/user-setting/mcp'), Component: lazy(() => import('@/pages/user-setting/mcp')),
}, },
{ {
path: `/user-setting${Routes.DataSource}`, path: `/user-setting${Routes.DataSource}`,
Component: () => import('@/pages/user-setting/data-source'), Component: lazy(() => import('@/pages/user-setting/data-source')),
}, },
], ],
errorElement: <FallbackComponent />,
}, },
{ {
path: `/user-setting${Routes.DataSource}${Routes.DataSourceDetailPage}`, path: `/user-setting${Routes.DataSource}${Routes.DataSourceDetailPage}`,
Component: () => Component: lazy(
import('@/pages/user-setting/data-source/data-source-detail-page'), () => import('@/pages/user-setting/data-source/data-source-detail-page'),
),
layout: false, layout: false,
errorElement: <FallbackComponent />,
}, },
{ {
path: Routes.Admin, path: Routes.Admin,
Component: () => import('@/pages/admin/layouts/root-layout'), Component: lazy(() => import('@/pages/admin/layouts/root-layout')),
errorElement: <FallbackComponent />,
children: [ children: [
{ {
path: Routes.Admin, path: Routes.Admin,
Component: () => import('@/pages/admin/login'), Component: lazy(() => import('@/pages/admin/login')),
}, },
{ {
path: Routes.Admin, path: Routes.Admin,
Component: () => import('@/pages/admin/layouts/authorized-layout'), Component: lazy(
() => import('@/pages/admin/layouts/authorized-layout'),
),
children: [ children: [
{ {
path: `${Routes.AdminUserManagement}/:id`, path: `${Routes.AdminUserManagement}/:id`,
Component: () => import('@/pages/admin/user-detail'), Component: lazy(() => import('@/pages/admin/user-detail')),
}, },
{ {
Component: () => import('@/pages/admin/layouts/navigation-layout'), Component: lazy(
() => import('@/pages/admin/layouts/navigation-layout'),
),
children: [ children: [
{ {
path: Routes.AdminServices, path: Routes.AdminServices,
Component: () => import('@/pages/admin/service-status'), Component: lazy(() => import('@/pages/admin/service-status')),
}, },
{ {
path: Routes.AdminUserManagement, path: Routes.AdminUserManagement,
Component: () => import('@/pages/admin/users'), Component: lazy(() => import('@/pages/admin/users')),
}, },
{ {
path: Routes.AdminSandboxSettings, path: Routes.AdminSandboxSettings,
Component: () => import('@/pages/admin/sandbox-settings'), Component: lazy(() => import('@/pages/admin/sandbox-settings')),
}, },
...(IS_ENTERPRISE ...(IS_ENTERPRISE
? [ ? [
{ {
path: Routes.AdminWhitelist, path: Routes.AdminWhitelist,
Component: () => import('@/pages/admin/whitelist'), Component: lazy(() => import('@/pages/admin/whitelist')),
}, },
{ {
path: Routes.AdminRoles, path: Routes.AdminRoles,
Component: () => import('@/pages/admin/roles'), Component: lazy(() => import('@/pages/admin/roles')),
}, },
{ {
path: Routes.AdminMonitoring, path: Routes.AdminMonitoring,
Component: () => import('@/pages/admin/monitoring'), Component: lazy(() => import('@/pages/admin/monitoring')),
}, },
] ]
: []), : []),
@ -439,24 +445,9 @@ const routeConfigOptions = [
], ],
}, },
], ],
} satisfies LazyRouteConfig, } satisfies RouteObject,
]; ];
const wrapRoutes = (routes: LazyRouteConfig[]): RouteObject[] =>
routes.map((item) => {
const { Component, children, ...rest } = item;
const next: RouteObject = { ...rest, errorElement: <FallbackComponent /> };
if (Component) {
next.Component = withLazyRoute(Component);
}
if (children) {
next.children = wrapRoutes(children);
}
return next;
});
const routeConfig = wrapRoutes(routeConfigOptions);
const routers = createBrowserRouter(routeConfig, { const routers = createBrowserRouter(routeConfig, {
basename: import.meta.env.VITE_BASE_URL || '/', basename: import.meta.env.VITE_BASE_URL || '/',
}); });

View File

@ -101,12 +101,6 @@ export default defineConfig(({ mode, command }) => {
experimentalMinChunkSize: 30 * 1024, experimentalMinChunkSize: 30 * 1024,
chunkSizeWarningLimit: 1000, chunkSizeWarningLimit: 1000,
rollupOptions: { rollupOptions: {
onwarn(warning, warn) {
if (warning.code === 'EMPTY_BUNDLE') {
return;
}
warn(warning);
},
output: { output: {
manualChunks(id) { manualChunks(id) {
// if (id.includes('src/components')) { // if (id.includes('src/components')) {