diff --git a/.env b/.env
new file mode 100644
index 0000000..bc9991a
--- /dev/null
+++ b/.env
@@ -0,0 +1,50 @@
+APP_NAME=X-Financial
+APP_ENV=local
+APP_DEBUG=true
+API_V1_PREFIX=/api/v1
+SETUP_COMPLETED=true
+VITE_SETUP_COMPLETED=true
+
+COMPANY_NAME=YGSOFT
+COMPANY_CODE=123
+ADMIN_EMAIL='admin@admin.com'
+VITE_COMPANY_NAME=YGSOFT
+VITE_COMPANY_CODE=123
+VITE_ADMIN_EMAIL='admin@admin.com'
+# Admin login credentials are stored separately under server/.secrets/
+
+WEB_HOST=10.10.10.122
+WEB_PORT=5173
+VITE_WEB_HOST=10.10.10.122
+VITE_WEB_PORT=5173
+
+SERVER_HOST=0.0.0.0
+SERVER_PORT=8000
+VITE_SERVER_HOST=0.0.0.0
+VITE_SERVER_PORT=8000
+SERVER_STARTUP_TIMEOUT=300
+SERVER_BLOCKING_STARTUP_TIMEOUT=12
+VITE_API_BASE_URL=/api/v1
+VITE_AUTH_IDLE_TIMEOUT_MINUTES=30
+ONLYOFFICE_ENABLED=true
+ONLYOFFICE_PUBLIC_URL=http://onlyoffice:80
+ONLYOFFICE_BACKEND_URL=http://main:8000
+ONLYOFFICE_JWT_SECRET=change-me-onlyoffice
+
+POSTGRES_HOST=10.10.10.189
+POSTGRES_PORT=5432
+POSTGRES_DB=postgres
+POSTGRES_USER=root
+POSTGRES_PASSWORD=8811614287327Leo
+VITE_POSTGRES_HOST=10.10.10.189
+VITE_POSTGRES_PORT=5432
+VITE_POSTGRES_DB=postgres
+VITE_POSTGRES_USER=root
+
+DATABASE_URL='postgresql+psycopg://root:8811614287327Leo@10.10.10.189:5432/postgres'
+SQLALCHEMY_ECHO=false
+
+REDIS_URL=
+VITE_REDIS_URL=
+
+CORS_ORIGINS='["http://10.10.10.122:5173"]'
diff --git a/docker-compose.yml b/docker-compose.yml
index cf3e8f2..5ca16d8 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,14 +6,15 @@ services:
depends_on:
onlyoffice:
condition: service_started
- environment:
- WEB_HOST: 0.0.0.0
- SERVER_HOST: 0.0.0.0
- SERVER_VENV_DIR: /tmp/x-financial-server-venv
- ONLYOFFICE_ENABLED: "${ONLYOFFICE_ENABLED:-true}"
- ONLYOFFICE_PUBLIC_URL: "${ONLYOFFICE_PUBLIC_URL:-http://127.0.0.1:${ONLYOFFICE_PORT:-8082}}"
- ONLYOFFICE_BACKEND_URL: "${ONLYOFFICE_BACKEND_URL:-http://main:${SERVER_PORT:-8000}}"
- ONLYOFFICE_JWT_SECRET: "${ONLYOFFICE_JWT_SECRET:-x-financial-onlyoffice-dev-secret}"
+ environment:
+ WEB_HOST: 0.0.0.0
+ SERVER_HOST: 0.0.0.0
+ SERVER_VENV_DIR: /tmp/x-financial-server-venv
+ X_FINANCIAL_PREFER_ENV_FILE: "true"
+ ONLYOFFICE_ENABLED: "${ONLYOFFICE_ENABLED:-true}"
+ ONLYOFFICE_PUBLIC_URL: "${ONLYOFFICE_PUBLIC_URL:-http://127.0.0.1:${ONLYOFFICE_PORT:-8082}}"
+ ONLYOFFICE_BACKEND_URL: "http://main:${SERVER_PORT:-8000}"
+ ONLYOFFICE_JWT_SECRET: "${ONLYOFFICE_JWT_SECRET:-x-financial-onlyoffice-dev-secret}"
ports:
- "${WEB_PORT:-5173}:${WEB_PORT:-5173}"
- "${SERVER_PORT:-8000}:${SERVER_PORT:-8000}"
@@ -38,25 +39,33 @@ services:
chmod +x /app/start.sh /app/web/web_start.sh /app/server/server_start.sh &&
cd /app &&
./start.sh all
- healthcheck:
- test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:${WEB_PORT:-5173}/ >/dev/null || exit 1"]
- interval: 15s
- timeout: 5s
- retries: 10
- start_period: 180s
-
- onlyoffice:
- image: onlyoffice/documentserver:latest
- container_name: x-financial-onlyoffice
+ healthcheck:
+ test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:${WEB_PORT:-5173}/ >/dev/null || exit 1"]
+ interval: 15s
+ timeout: 5s
+ retries: 10
+ start_period: 180s
+ networks:
+ - financial-internal
+
+ onlyoffice:
+ image: onlyoffice/documentserver:latest
+ container_name: x-financial-onlyoffice
restart: unless-stopped
environment:
JWT_ENABLED: "true"
JWT_SECRET: "${ONLYOFFICE_JWT_SECRET:-x-financial-onlyoffice-dev-secret}"
ports:
- "${ONLYOFFICE_PORT:-8082}:80"
- healthcheck:
- test: ["CMD-SHELL", "curl -fsS http://127.0.0.1/healthcheck >/dev/null || exit 1"]
- interval: 15s
- timeout: 5s
- retries: 10
- start_period: 60s
+ healthcheck:
+ test: ["CMD-SHELL", "curl -fsS http://127.0.0.1/healthcheck >/dev/null || exit 1"]
+ interval: 15s
+ timeout: 5s
+ retries: 10
+ start_period: 60s
+ networks:
+ - financial-internal
+
+networks:
+ financial-internal:
+ name: financial-internal
diff --git a/nul b/nul
new file mode 100644
index 0000000..49d60e3
--- /dev/null
+++ b/nul
@@ -0,0 +1 @@
+/usr/bin/bash: line 1: rg: command not found
diff --git a/server/server_start.sh b/server/server_start.sh
index cc29713..1f8f3dd 100755
--- a/server/server_start.sh
+++ b/server/server_start.sh
@@ -94,6 +94,13 @@ ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET=false
ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET=false
ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET=false
ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET=false
+PREFER_ENV_FILE_FOR_ONLYOFFICE=false
+
+case "${X_FINANCIAL_PREFER_ENV_FILE:-false}" in
+ 1|true|TRUE|yes|YES|on|ON)
+ PREFER_ENV_FILE_FOR_ONLYOFFICE=true
+ ;;
+esac
if [ "${SERVER_HOST+x}" = x ]; then
ENV_OVERRIDE_SERVER_HOST_SET=true
@@ -110,22 +117,22 @@ if [ "${DATABASE_URL+x}" = x ]; then
ENV_OVERRIDE_DATABASE_URL="$DATABASE_URL"
fi
-if [ "${ONLYOFFICE_ENABLED+x}" = x ]; then
+if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_ENABLED+x}" = x ]; then
ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET=true
ENV_OVERRIDE_ONLYOFFICE_ENABLED="$ONLYOFFICE_ENABLED"
fi
-if [ "${ONLYOFFICE_PUBLIC_URL+x}" = x ]; then
+if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_PUBLIC_URL+x}" = x ]; then
ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET=true
ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL="$ONLYOFFICE_PUBLIC_URL"
fi
-if [ "${ONLYOFFICE_BACKEND_URL+x}" = x ]; then
+if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_BACKEND_URL+x}" = x ]; then
ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET=true
ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL="$ONLYOFFICE_BACKEND_URL"
fi
-if [ "${ONLYOFFICE_JWT_SECRET+x}" = x ]; then
+if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_JWT_SECRET+x}" = x ]; then
ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET=true
ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET="$ONLYOFFICE_JWT_SECRET"
fi
diff --git a/server/src/app/core/config.py b/server/src/app/core/config.py
index 3d24fdf..a692c40 100644
--- a/server/src/app/core/config.py
+++ b/server/src/app/core/config.py
@@ -1,22 +1,32 @@
-from __future__ import annotations
-
-from functools import lru_cache
-from os import environ
-from pathlib import Path
-
-from pydantic import Field
-from pydantic_settings import BaseSettings, SettingsConfigDict
-
-SERVER_DIR = Path(__file__).resolve().parents[3]
-ROOT_DIR = SERVER_DIR.parent
+from __future__ import annotations
+
+from os import environ
+from pathlib import Path
+
+from dotenv import dotenv_values
+from pydantic import Field
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+SERVER_DIR = Path(__file__).resolve().parents[3]
+ROOT_DIR = SERVER_DIR.parent
+DEFAULT_ENV_FILES = (ROOT_DIR / ".env", SERVER_DIR / ".env")
+ONLYOFFICE_FIELD_NAMES = {
+ "ONLYOFFICE_ENABLED": "onlyoffice_enabled",
+ "ONLYOFFICE_PUBLIC_URL": "onlyoffice_public_url",
+ "ONLYOFFICE_BACKEND_URL": "onlyoffice_backend_url",
+ "ONLYOFFICE_JWT_SECRET": "onlyoffice_jwt_secret",
+}
+
+_settings_cache: Settings | None = None
+_settings_cache_signature: tuple[tuple[str, bool, int | None, int | None], ...] | None = None
-class Settings(BaseSettings):
- model_config = SettingsConfigDict(
- env_file=(ROOT_DIR / ".env", SERVER_DIR / ".env"),
- env_file_encoding="utf-8",
- extra="ignore",
- )
+class Settings(BaseSettings):
+ model_config = SettingsConfigDict(
+ env_file=DEFAULT_ENV_FILES,
+ env_file_encoding="utf-8",
+ extra="ignore",
+ )
app_name: str = Field(default="X-Financial Server", alias="APP_NAME")
app_env: str = Field(default="local", alias="APP_ENV")
@@ -73,16 +83,80 @@ class Settings(BaseSettings):
if not path.is_absolute():
path = SERVER_DIR / path
return path.resolve()
-
-
-@lru_cache
-def get_settings() -> Settings:
- return Settings()
-
-
-def refresh_settings(updated_values: dict[str, str]) -> Settings:
- for key, value in updated_values.items():
- environ[key] = value
-
- get_settings.cache_clear()
- return get_settings()
+
+def _resolve_env_files() -> tuple[Path, ...]:
+ env_files = Settings.model_config.get("env_file") or ()
+ return tuple(Path(item) for item in env_files)
+
+
+def _build_settings_signature() -> tuple[tuple[str, bool, int | None, int | None], ...]:
+ signature: list[tuple[str, bool, int | None, int | None]] = []
+
+ for env_file in _resolve_env_files():
+ if not env_file.exists():
+ signature.append((str(env_file), False, None, None))
+ continue
+
+ stat = env_file.stat()
+ signature.append((str(env_file), True, stat.st_mtime_ns, stat.st_size))
+
+ return tuple(signature)
+
+
+def _parse_onlyoffice_enabled(value: object) -> bool:
+ return str(value).strip().lower() in {"1", "true", "yes", "on"}
+
+
+def _load_onlyoffice_env_file_overrides() -> dict[str, object]:
+ overrides: dict[str, object] = {}
+
+ for env_file in _resolve_env_files():
+ if not env_file.exists():
+ continue
+
+ values = dotenv_values(env_file)
+ for alias, field_name in ONLYOFFICE_FIELD_NAMES.items():
+ if alias not in values:
+ continue
+
+ value = values[alias]
+ if field_name == "onlyoffice_enabled":
+ overrides[field_name] = _parse_onlyoffice_enabled(value)
+ else:
+ overrides[field_name] = "" if value is None else str(value)
+
+ return overrides
+
+
+def _clear_settings_cache() -> None:
+ global _settings_cache, _settings_cache_signature
+
+ _settings_cache = None
+ _settings_cache_signature = None
+
+
+def get_settings() -> Settings:
+ global _settings_cache, _settings_cache_signature
+
+ signature = _build_settings_signature()
+ if _settings_cache is None or _settings_cache_signature != signature:
+ settings = Settings()
+ onlyoffice_overrides = _load_onlyoffice_env_file_overrides()
+ if onlyoffice_overrides:
+ settings = settings.model_copy(update=onlyoffice_overrides)
+
+ _settings_cache = settings
+ _settings_cache_signature = signature
+
+ return _settings_cache
+
+
+get_settings.cache_clear = _clear_settings_cache # type: ignore[attr-defined]
+
+
+def refresh_settings(updated_values: dict[str, str]) -> Settings:
+ for key, value in updated_values.items():
+ environ[key] = value
+
+ get_settings.cache_clear()
+ return get_settings()
diff --git a/server/src/app/services/knowledge.py b/server/src/app/services/knowledge.py
index fbcd5f4..ca3760f 100644
--- a/server/src/app/services/knowledge.py
+++ b/server/src/app/services/knowledge.py
@@ -232,19 +232,43 @@ class KnowledgeService:
return file_path, entry["mime_type"], entry["original_name"]
- def build_onlyoffice_config(
- self,
- document_id: str,
- current_user: CurrentUserContext,
- ) -> KnowledgeOnlyOfficeConfigRead:
- self.ensure_library_ready()
- settings = get_settings()
- if not settings.onlyoffice_enabled:
- raise ValueError("ONLYOFFICE 预览未启用。")
- if not settings.onlyoffice_public_url or not settings.onlyoffice_backend_url:
- raise ValueError("ONLYOFFICE 地址配置不完整。")
- if not settings.onlyoffice_jwt_secret:
- raise ValueError("ONLYOFFICE JWT 密钥未配置。")
+ def build_onlyoffice_config(
+ self,
+ document_id: str,
+ current_user: CurrentUserContext,
+ ) -> KnowledgeOnlyOfficeConfigRead:
+ self.ensure_library_ready()
+ settings = get_settings()
+ if not settings.onlyoffice_enabled:
+ logger.warning(
+ "ONLYOFFICE disabled in runtime config doc=%s enabled=%s public_url=%s backend_url=%s jwt_set=%s",
+ document_id,
+ settings.onlyoffice_enabled,
+ settings.onlyoffice_public_url,
+ settings.onlyoffice_backend_url,
+ bool(settings.onlyoffice_jwt_secret),
+ )
+ raise ValueError("ONLYOFFICE 预览未启用。")
+ if not settings.onlyoffice_public_url or not settings.onlyoffice_backend_url:
+ logger.warning(
+ "ONLYOFFICE config incomplete doc=%s enabled=%s public_url=%s backend_url=%s jwt_set=%s",
+ document_id,
+ settings.onlyoffice_enabled,
+ settings.onlyoffice_public_url,
+ settings.onlyoffice_backend_url,
+ bool(settings.onlyoffice_jwt_secret),
+ )
+ raise ValueError("ONLYOFFICE 地址配置不完整。")
+ if not settings.onlyoffice_jwt_secret:
+ logger.warning(
+ "ONLYOFFICE JWT missing doc=%s enabled=%s public_url=%s backend_url=%s jwt_set=%s",
+ document_id,
+ settings.onlyoffice_enabled,
+ settings.onlyoffice_public_url,
+ settings.onlyoffice_backend_url,
+ bool(settings.onlyoffice_jwt_secret),
+ )
+ raise ValueError("ONLYOFFICE JWT 密钥未配置。")
index = self._load_index()
entry = self._require_entry(index, document_id)
@@ -263,42 +287,41 @@ class KnowledgeService:
callback_url = (
f"{backend_base_url}{settings.api_v1_prefix}/knowledge/documents/{document_id}/onlyoffice/callback"
)
- can_edit = current_user.is_admin or "manager" in current_user.role_codes
- document_key = self._build_onlyoffice_document_key(entry)
-
- config: dict[str, Any] = {
- "documentType": document_type,
- "document": {
+ document_key = self._build_onlyoffice_document_key(entry)
+
+ config: dict[str, Any] = {
+ "documentType": document_type,
+ "document": {
"fileType": extension,
"key": document_key,
- "title": entry["original_name"],
- "url": document_url,
- "permissions": {
- "download": True,
- "edit": can_edit,
- "print": True,
- "copy": True,
+ "title": entry["original_name"],
+ "url": document_url,
+ "permissions": {
+ "download": True,
+ "edit": False,
+ "print": True,
+ "copy": True,
+ },
+ },
+ "editorConfig": {
+ "mode": "view",
+ "lang": "zh-CN",
+ "callbackUrl": callback_url,
+ "user": {
+ "id": current_user.username,
+ "name": current_user.name,
},
- },
- "editorConfig": {
- "mode": "edit" if can_edit else "view",
- "lang": "zh-CN",
- "callbackUrl": callback_url,
- "user": {
- "id": current_user.username,
- "name": current_user.name,
- },
- "customization": {
- "compactHeader": True,
- "compactToolbar": True,
- "toolbarNoTabs": False,
- "autosave": can_edit,
- "forcesave": can_edit,
- },
- },
- "width": "100%",
- "height": "100%",
- }
+ "customization": {
+ "compactHeader": True,
+ "compactToolbar": True,
+ "toolbarNoTabs": False,
+ "autosave": False,
+ "forcesave": False,
+ },
+ },
+ "width": "100%",
+ "height": "100%",
+ }
config["token"] = jwt.encode(config, settings.onlyoffice_jwt_secret, algorithm="HS256")
return KnowledgeOnlyOfficeConfigRead(
diff --git a/server/src/x_financial_server.egg-info/PKG-INFO b/server/src/x_financial_server.egg-info/PKG-INFO
index 395817c..f6c2a17 100644
--- a/server/src/x_financial_server.egg-info/PKG-INFO
+++ b/server/src/x_financial_server.egg-info/PKG-INFO
@@ -9,6 +9,7 @@ Requires-Dist: uvicorn[standard]<1.0.0,>=0.30.0
Requires-Dist: sqlalchemy<3.0.0,>=2.0.36
Requires-Dist: alembic<2.0.0,>=1.14.0
Requires-Dist: psycopg[binary]<4.0.0,>=3.2.0
+Requires-Dist: PyJWT<3.0.0,>=2.9.0
Requires-Dist: pydantic-settings<3.0.0,>=2.6.0
Requires-Dist: python-dotenv<2.0.0,>=1.0.1
Requires-Dist: email-validator<3.0.0,>=2.2.0
diff --git a/server/src/x_financial_server.egg-info/SOURCES.txt b/server/src/x_financial_server.egg-info/SOURCES.txt
index 3211542..794885f 100644
--- a/server/src/x_financial_server.egg-info/SOURCES.txt
+++ b/server/src/x_financial_server.egg-info/SOURCES.txt
@@ -12,6 +12,7 @@ src/app/api/v1/endpoints/auth.py
src/app/api/v1/endpoints/bootstrap.py
src/app/api/v1/endpoints/employees.py
src/app/api/v1/endpoints/health.py
+src/app/api/v1/endpoints/knowledge.py
src/app/api/v1/endpoints/reimbursements.py
src/app/api/v1/endpoints/settings.py
src/app/core/__init__.py
@@ -45,12 +46,14 @@ src/app/schemas/__init__.py
src/app/schemas/auth.py
src/app/schemas/bootstrap.py
src/app/schemas/employee.py
+src/app/schemas/knowledge.py
src/app/schemas/reimbursement.py
src/app/schemas/settings.py
src/app/services/__init__.py
src/app/services/auth.py
src/app/services/employee.py
src/app/services/employee_seed.py
+src/app/services/knowledge.py
src/app/services/model_connectivity.py
src/app/services/reimbursement.py
src/app/services/settings.py
@@ -62,5 +65,6 @@ src/x_financial_server.egg-info/top_level.txt
tests/test_auth_service.py
tests/test_employee_service.py
tests/test_imports.py
+tests/test_server_start_dependencies.py
tests/test_settings_persistence.py
tests/test_settings_service.py
\ No newline at end of file
diff --git a/server/src/x_financial_server.egg-info/requires.txt b/server/src/x_financial_server.egg-info/requires.txt
index e3415eb..0b12d2e 100644
--- a/server/src/x_financial_server.egg-info/requires.txt
+++ b/server/src/x_financial_server.egg-info/requires.txt
@@ -3,6 +3,7 @@ uvicorn[standard]<1.0.0,>=0.30.0
sqlalchemy<3.0.0,>=2.0.36
alembic<2.0.0,>=1.14.0
psycopg[binary]<4.0.0,>=3.2.0
+PyJWT<3.0.0,>=2.9.0
pydantic-settings<3.0.0,>=2.6.0
python-dotenv<2.0.0,>=1.0.1
email-validator<3.0.0,>=2.2.0
diff --git a/server/storage/knowledge/.index.json b/server/storage/knowledge/.index.json
index 7f72f04..1cd86bc 100644
--- a/server/storage/knowledge/.index.json
+++ b/server/storage/knowledge/.index.json
@@ -14,6 +14,34 @@
"updated_at": "2026-05-09T05:46:24.699125+00:00",
"uploaded_by": "admin",
"version_number": 1
+ },
+ {
+ "id": "6cad2936d57242d29d26f6fbb6314767",
+ "folder": "财务知识库",
+ "original_name": "2508.19855v3.pdf",
+ "stored_name": "6cad2936d57242d29d26f6fbb6314767__2508.19855v3.pdf",
+ "mime_type": "application/pdf",
+ "extension": "pdf",
+ "size_bytes": 4097809,
+ "sha256": "9061363b164aaba132454e239ecc107076c81f61ecab1eb39cb43405d481e46a",
+ "created_at": "2026-05-09T06:06:51.631071+00:00",
+ "updated_at": "2026-05-09T06:06:51.631071+00:00",
+ "uploaded_by": "admin",
+ "version_number": 1
+ },
+ {
+ "id": "b01fe587d3d941f0a25d500751b27094",
+ "folder": "财务知识库",
+ "original_name": "面向财务领域的大语言模型 Fin-R1 研究内容与实施计划 (1).docx",
+ "stored_name": "b01fe587d3d941f0a25d500751b27094__面向财务领域的大语言模型 Fin-R1 研究内容与实施计划 (1).docx",
+ "mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "extension": "docx",
+ "size_bytes": 35521,
+ "sha256": "b300ba9c4c5bb03f4cbb27b52eea1932d1594398ae7b0f0c51c6c250deaceab4",
+ "created_at": "2026-05-09T06:07:10.525556+00:00",
+ "updated_at": "2026-05-09T07:17:28.581707+00:00",
+ "uploaded_by": "admin",
+ "version_number": 3
}
]
}
\ No newline at end of file
diff --git a/server/storage/knowledge/财务知识库/6cad2936d57242d29d26f6fbb6314767__2508.19855v3.pdf b/server/storage/knowledge/财务知识库/6cad2936d57242d29d26f6fbb6314767__2508.19855v3.pdf
new file mode 100644
index 0000000..bfd7364
--- /dev/null
+++ b/server/storage/knowledge/财务知识库/6cad2936d57242d29d26f6fbb6314767__2508.19855v3.pdf
@@ -0,0 +1,19066 @@
+%PDF-1.5
+%
+1 0 obj
+<< /Metadata 3 0 R /Names 4 0 R /OpenAction 5 0 R /Outlines 6 0 R /PageMode /UseOutlines /Pages 7 0 R /Type /Catalog >>
+endobj
+2 0 obj
+<< /Author (Junnan Dong; Siyu An; Yifei Yu; Qian-Wen Zhang; Linhao Luo; Xiao Huang; Yunsheng Wu; Di Yin; Xing Sun) /Creator (arXiv GenPDF \(tex2pdf:\)) /DOI (https://doi.org/10.48550/arXiv.2508.19855) /License (http://creativecommons.org/licenses/by/4.0/) /PTEX.Fullbanner (This is pdfTeX, Version 3.141592653-2.6-1.40.25 \(TeX Live 2023\) kpathsea version 6.3.5) /Producer (pikepdf 8.15.1) /Title (Youtu-GraphRAG: Vertically Unified Agents for Graph Retrieval-Augmented Complex Reasoning) /Trapped /False /arXivID (https://arxiv.org/abs/2508.19855v3) >>
+endobj
+3 0 obj
+<< /Subtype /XML /Type /Metadata /Length 1790 >>
+stream
+
+
+
+ Youtu-GraphRAG: Vertically Unified Agents for Graph Retrieval-Augmented Complex ReasoningJunnan DongSiyu AnYifei YuQian-Wen ZhangLinhao LuoXiao HuangYunsheng WuDi YinXing Sunhttp://creativecommons.org/licenses/by/4.0/cs.IR
+
+
+
+
+endstream
+endobj
+4 0 obj
+<< /Dests 8 0 R >>
+endobj
+5 0 obj
+<< /D [ 9 0 R /Fit ] /S /GoTo >>
+endobj
+6 0 obj
+<< /Count 8 /First 10 0 R /Last 11 0 R /Type /Outlines >>
+endobj
+7 0 obj
+<< /Count 19 /Kids [ 12 0 R 13 0 R 14 0 R 15 0 R ] /Type /Pages >>
+endobj
+8 0 obj
+<< /Kids [ 16 0 R 17 0 R 18 0 R ] /Limits [ (Doc-Start) (table.caption.8) ] >>
+endobj
+9 0 obj
+<< /Annots [ 19 0 R 20 0 R 21 0 R 22 0 R 23 0 R 24 0 R 25 0 R 26 0 R 27 0 R 28 0 R 29 0 R 30 0 R 31 0 R 32 0 R 33 0 R 34 0 R 35 0 R 36 0 R 37 0 R 38 0 R 39 0 R 40 0 R ] /Contents [ 41 0 R 42 0 R 43 0 R 44 0 R ] /Group 45 0 R /MediaBox [ 0 0 612 792 ] /Parent 12 0 R /Resources 46 0 R /Type /Page >>
+endobj
+10 0 obj
+<< /A 47 0 R /Next 48 0 R /Parent 6 0 R /Title 49 0 R >>
+endobj
+11 0 obj
+<< /A 50 0 R /Parent 6 0 R /Prev 51 0 R /Title 52 0 R >>
+endobj
+12 0 obj
+<< /Count 6 /Kids [ 9 0 R 53 0 R 54 0 R 55 0 R 56 0 R 57 0 R ] /Parent 7 0 R /Type /Pages >>
+endobj
+13 0 obj
+<< /Count 6 /Kids [ 58 0 R 59 0 R 60 0 R 61 0 R 62 0 R 63 0 R ] /Parent 7 0 R /Type /Pages >>
+endobj
+14 0 obj
+<< /Count 6 /Kids [ 64 0 R 65 0 R 66 0 R 67 0 R 68 0 R 69 0 R ] /Parent 7 0 R /Type /Pages >>
+endobj
+15 0 obj
+<< /Count 1 /Kids [ 70 0 R ] /Parent 7 0 R /Type /Pages >>
+endobj
+16 0 obj
+<< /Kids [ 71 0 R 72 0 R 73 0 R 74 0 R 75 0 R 76 0 R ] /Limits [ (Doc-Start) (equation.3.2) ] >>
+endobj
+17 0 obj
+<< /Kids [ 77 0 R 78 0 R 79 0 R 80 0 R 81 0 R 82 0 R ] /Limits [ (equation.3.3) (section.2) ] >>
+endobj
+18 0 obj
+<< /Kids [ 83 0 R 84 0 R 85 0 R 86 0 R 87 0 R ] /Limits [ (section.3) (table.caption.8) ] >>
+endobj
+19 0 obj
+<< /A << /S /URI /Type /Action /URI (https://github.com/TencentCloudADP/Youtu-GraphRAG) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 125.58 275.219 358.675 287.273 ] /Subtype /Link /Type /Annot >>
+endobj
+20 0 obj
+<< /A << /S /URI /Type /Action /URI (https://huggingface.co/datasets/Youtu-Graph/AnonyRAG) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 123.571 263.263 370.784 275.318 ] /Subtype /Link /Type /Annot >>
+endobj
+21 0 obj
+<< /A << /D (cite.graphrag-bench) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 317.077 162.499 364.209 174.509 ] /Subtype /Link /Type /Annot >>
+endobj
+22 0 obj
+<< /A << /D (cite.graphrag-bench) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 367.765 162.499 390.081 174.509 ] /Subtype /Link /Type /Annot >>
+endobj
+23 0 obj
+<< /A << /D (cite.pan2024unifying) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 393.637 162.499 437.537 174.509 ] /Subtype /Link /Type /Annot >>
+endobj
+24 0 obj
+<< /A << /D (cite.pan2024unifying) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 441.093 162.499 463.41 174.509 ] /Subtype /Link /Type /Annot >>
+endobj
+25 0 obj
+<< /A << /D (cite.wang2024knowledge) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 357.015 150.544 409.249 162.554 ] /Subtype /Link /Type /Annot >>
+endobj
+26 0 obj
+<< /A << /D (cite.wang2024knowledge) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 412.594 150.544 434.91 162.554 ] /Subtype /Link /Type /Annot >>
+endobj
+27 0 obj
+<< /A << /D (cite.dong2024knowgpt) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 438.254 150.544 493.935 162.554 ] /Subtype /Link /Type /Annot >>
+endobj
+28 0 obj
+<< /A << /D (cite.dong2024knowgpt) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 497.28 150.544 519.596 162.554 ] /Subtype /Link /Type /Annot >>
+endobj
+29 0 obj
+<< /A << /D (cite.gretriever) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 447.45 138.589 486.158 150.599 ] /Subtype /Link /Type /Annot >>
+endobj
+30 0 obj
+<< /A << /D (cite.gretriever) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 489.156 138.589 511.273 150.599 ] /Subtype /Link /Type /Annot >>
+endobj
+31 0 obj
+<< /A << /D (cite.dong2023hierarchy) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 514.271 138.589 540.996 150.599 ] /Subtype /Link /Type /Annot >>
+endobj
+32 0 obj
+<< /A << /D (cite.dong2023hierarchy) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 71.004 126.634 93.702 138.643 ] /Subtype /Link /Type /Annot >>
+endobj
+33 0 obj
+<< /A << /D (cite.dong2023hierarchy) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 96.674 126.634 118.393 138.643 ] /Subtype /Link /Type /Annot >>
+endobj
+34 0 obj
+<< /A << /D (cite.graphragsurvey1) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 369.549 114.678 417.355 126.688 ] /Subtype /Link /Type /Annot >>
+endobj
+35 0 obj
+<< /A << /D (cite.graphragsurvey1) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 420.359 114.678 442.436 126.688 ] /Subtype /Link /Type /Annot >>
+endobj
+36 0 obj
+<< /A << /D (cite.graphragsurvey2) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 445.44 114.678 490.163 126.688 ] /Subtype /Link /Type /Annot >>
+endobj
+37 0 obj
+<< /A << /D (cite.graphragsurvey2) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 493.167 114.678 515.244 126.688 ] /Subtype /Link /Type /Annot >>
+endobj
+38 0 obj
+<< /A << /D (cite.dong2024modality) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 430.633 102.723 478.993 114.733 ] /Subtype /Link /Type /Annot >>
+endobj
+39 0 obj
+<< /A << /D (cite.dong2024modality) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 481.614 102.723 503.133 114.733 ] /Subtype /Link /Type /Annot >>
+endobj
+40 0 obj
+<< /A << /S /URI /URI (https://arxiv.org/abs/2508.19855v3) >> /BS << /W 0 >> /NM (fitz-L0) /Rect [ 12 230.46002 32 561.54 ] /Subtype /Link >>
+endobj
+41 0 obj
+<< /Length 10 /Filter /FlateDecode >>
+stream
+x+ |
+endstream
+endobj
+42 0 obj
+<< /Filter /FlateDecode /Length 3806 >>
+stream
+xZYܶ~_WUK.qr^,K>U۱].3ÈxHj
YYR6IYnt`o0'N,/XAgM=ăg~O~^Gγ%d=f|sq5~yud$}Ll_ܟۡoa77`=_-O}e/9/Ĺo V]9_K9~"'/cid$X%E4;!(kaH>gT`h+$,,Ix =(zo18eU=u)
^lɮhH
K1ª/fAXY=v5[bOPUumS6;o$06 'l&k@,
+g-ѡG~CF~Dy,v1ʃ_HF2~E_4ؗXK#Ո0dO.Ei,T,Y2~,0>;/@Z*'slYkC{VȄ>?6@.C? ԎTZlL~D\ՊsKM/@Y?xVA[&aj_䙢aZbFx*%S>Y$ M=$w haLCu8zPHdʝbLnQ39Qq(cȵMl>$Fߓ~;t*`%}ė
+(|u2P3(ݞ}5a8/qʨfG<=f(}q3qFmOߣ{0,eV}YTj{NonC_DT∽&t"/-ݾ0fi!n_n{clmCqjasN/|ǝ1f{h;csN)1[o*fFg,㐱CK1۔"1#L},i>fV85+Zm
LuGZ7iq&=VlS_X)p<}#/$JJp$i;H qB\N,U9ZγnYB̼y1mq&2:]83yA"H8v͐ZoCJJܛvh!͆o,
+y3:]*6iKVovGixFEql zfOeUR//ʐ x;m /% 7`#MYEr!S^KH#zEt~q8t}fNf1ElPzxsJ}7=;k_7.@4yR8u2t*G zJHB{1%n0͠wuf5|V
tmPu(;YeMVLկ7Ypl>#GS
#t$9?zZ:
+L>*DfT8dzL먝ϾJ"U>8.c6 ܴZF̵oޙȬ|iG6:Q53C@ӴA#aOMtC]gpdQᣕ kr-OFwp>j9
+f8
EWNu;nXJYB%t
+}(.fqQD{"{&U7q