马一丁

Fixed the issue of GraphRAG not being read in full-screen mode

... ... @@ -420,9 +420,16 @@ class ReportAgent:
error_log_dir=self.config.JSON_ERROR_LOG_DIR,
)
def generate_report(self, query: str, reports: List[Any], forum_logs: str = "",
custom_template: str = "", save_report: bool = True,
stream_handler: Optional[Callable[[str, Dict[str, Any]], None]] = None) -> str:
def generate_report(
self,
query: str,
reports: List[Any],
forum_logs: str = "",
custom_template: str = "",
save_report: bool = True,
stream_handler: Optional[Callable[[str, Dict[str, Any]], None]] = None,
report_id: Optional[str] = None
) -> str:
"""
生成综合报告(章节JSON → IR → HTML)。
... ... @@ -440,6 +447,7 @@ class ReportAgent:
custom_template: 用户指定的Markdown模板,如为空则交由模板节点自动挑选。
save_report: 是否在生成后自动将HTML、IR与状态写入磁盘。
stream_handler: 可选的流式事件回调,接收阶段标签与payload,用于UI实时展示。
report_id: 外部透传的任务ID,用于与前端/SSE保持一致并复用同一个目录。
返回:
dict: 包含 `html_content` 以及HTML/IR/状态文件路径的字典;若 `save_report=False` 则仅返回HTML字符串。
... ... @@ -448,7 +456,16 @@ class ReportAgent:
Exception: 任一子节点或渲染阶段失败时抛出,外层调用方负责兜底。
"""
start_time = datetime.now()
report_id = f"report-{uuid4().hex[:8]}"
report_id_value = (report_id or "").strip()
if report_id_value:
# 仅保留易读且安全的字符,确保可直接作为目录名复用
report_id_value = "".join(
c if c.isalnum() or c in ("-", "_") else "_" for c in report_id_value
) or f"report-{uuid4().hex[:8]}"
else:
report_id_value = f"report-{uuid4().hex[:8]}"
report_id = report_id_value
self.state.task_id = report_id
self.state.query = query
self.state.metadata.query = query
... ...
... ... @@ -489,7 +489,8 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = "
forum_logs=content['forum_logs'],
custom_template=custom_template,
save_report=True,
stream_handler=stream_handler
stream_handler=stream_handler,
report_id=task.task_id
)
break
except ChapterJsonParseError as err:
... ...
... ... @@ -293,6 +293,28 @@ class GraphStorage:
"""图谱存储管理器"""
FILENAME = "graphrag.json"
@staticmethod
def _normalize_identifier(value: str) -> str:
"""统一规约ID,去除分隔符便于模糊匹配。"""
return re.sub(r'[^a-zA-Z0-9]', '', str(value or '')).lower()
def _graph_file_matches(self, graph_path: Path, normalized_target: str) -> bool:
"""检查图文件中的 task_id/report_id 是否与目标匹配。"""
try:
with open(graph_path, 'r', encoding='utf-8') as f:
data = json.load(f)
candidates = [
data.get('task_id'),
data.get('report_id'),
data.get('metadata', {}).get('report_id') if isinstance(data.get('metadata'), dict) else None
]
for candidate in candidates:
if candidate and self._normalize_identifier(candidate) == normalized_target:
return True
except Exception:
return False
return False
@property
def chapters_dir(self) -> Path:
... ... @@ -377,17 +399,17 @@ class GraphStorage:
chapters_dir = self.chapters_dir
if not chapters_dir.exists():
return None
# 兼容不同分隔符(report-xxx 与 report_xxx)以及简化匹配
if not report_id:
return None
normalized_target = re.sub(r'[-_]', '', str(report_id)).lower()
# 兼容不同分隔符(report-xxx 与 report_xxx)以及简化匹配
normalized_target = self._normalize_identifier(report_id)
alt_targets = {
report_id,
str(report_id),
str(report_id).replace('_', '-'),
str(report_id).replace('-', '_'),
normalized_target,
}
fallback_match: Optional[Path] = None
# 查找匹配报告ID的目录
for run_dir in chapters_dir.iterdir():
... ... @@ -395,7 +417,7 @@ class GraphStorage:
continue
name = run_dir.name
normalized_name = re.sub(r'[-_]', '', name).lower()
normalized_name = self._normalize_identifier(name)
# 检查目录名是否包含报告ID或归一化后相等
if (
... ... @@ -405,8 +427,14 @@ class GraphStorage:
graph_path = run_dir / self.FILENAME
if graph_path.exists():
return graph_path
# 若目录名不匹配,则尝试读取文件内容比对 task_id/report_id
graph_path = run_dir / self.FILENAME
if graph_path.exists() and not fallback_match:
if self._graph_file_matches(graph_path, normalized_target):
fallback_match = graph_path
return None
return fallback_match
def find_latest_graph(self) -> Optional[Path]:
"""
... ...
... ... @@ -633,21 +633,34 @@
// 加载图谱数据
async function loadGraphData(options = {}) {
const { fromPoll = false, fromManual = false } = options;
const { fromPoll = false, fromManual = false, allowFallback = true } = options;
// 仅在首次或未加载成功时展示大遮罩
if (!graphReady || !fromPoll) {
showLoading(true);
}
try {
const url = reportId
? `/api/graph/${reportId}`
: '/api/graph/latest';
const response = await fetch(url, { cache: 'no-store' });
const data = await response.json();
const fetchGraph = async (id) => {
const url = id ? `/api/graph/${id}` : '/api/graph/latest';
const response = await fetch(url, { cache: 'no-store' });
const data = await response.json();
if (!response.ok || !data.success || !data.graph) {
return null;
}
return data;
};
let data = await fetchGraph(reportId);
let usedFallback = false;
if ((!data || !data.graph) && allowFallback) {
data = await fetchGraph(null);
usedFallback = !!(data && data.graph);
}
if (data.success && data.graph) {
if (data && data.graph) {
if (data.report_id) {
reportId = data.report_id;
}
allNodes = data.graph.nodes;
allEdges = data.graph.edges;
... ... @@ -664,7 +677,7 @@
graphReady = true;
stopGraphPolling();
if (fromManual) {
showToast('已刷新最新图谱');
showToast(usedFallback ? '未找到指定图谱,已切换至最新版本' : '已刷新最新图谱');
}
} else {
if (!graphReady) {
... ...
... ... @@ -5586,14 +5586,24 @@ function getConsoleContainer() {
try {
const targetTaskId = graphPanelTaskId || (lastCompletedReportTask ? lastCompletedReportTask.task_id : null);
let data = await fetchGraphData(targetTaskId);
if (!data && allowFallback && targetTaskId) {
let data = null;
let usedFallback = false;
if (targetTaskId) {
data = await fetchGraphData(targetTaskId);
}
if ((!data || !data.graph) && allowFallback) {
data = await fetchGraphData(null);
usedFallback = !!(data && data.graph);
}
if (data && data.graph) {
graphPanelTaskId = targetTaskId || data.report_id || graphPanelTaskId;
const resolvedId = data.report_id || targetTaskId || graphPanelTaskId;
if (resolvedId) {
graphPanelTaskId = resolvedId;
}
renderGraphPanel(data.graph);
setGraphPanelState('ready');
setGraphPanelState('ready', usedFallback ? '已切换到最新可用的知识图谱' : '');
} else {
updateGraphStats({ nodes: [], edges: [] });
setGraphPanelState('idle', '暂未找到知识图谱,请生成报告后刷新');
... ...