mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-20 14:41:05 +08:00
feat(agent): ship the Go agent canvas port — eino interrupt/resume + Redis check-pointing (#16035)
Replaces the Python agent canvas runtime with a Go implementation that runs inside `cmd/server_main`. The canvas compiles into an eino Workflow that pauses on wait-for-user via native Interrupt/Resume (no sentinel flag) and resumes from a Redis-backed CheckPointStore. All 21 Python agent components and ~35 tools are ported with functional parity. Sandbox providers now read their JSON config from the admin-panel system_settings table with env fallback. 234 files / +35,413 / -6,111. All Go files are gofmt-clean (CI gate added); drops the v2 DSL E2E step and the gap-analysis plan (both redundant after the port ships). ## Type of change - [x] Refactoring - [x] New feature - [x] Bug fix 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -11,23 +11,30 @@ import { get, isEmpty } from 'lodash';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export function filterAllUpstreamNodeIds(edges: Edge[], nodeIds: string[]) {
|
||||
return nodeIds.reduce<string[]>((pre, nodeId) => {
|
||||
const currentEdges = edges.filter((x) => x.target === nodeId);
|
||||
|
||||
const upstreamNodeIds: string[] = currentEdges.map((x) => x.source);
|
||||
|
||||
const ids = upstreamNodeIds.concat(
|
||||
filterAllUpstreamNodeIds(edges, upstreamNodeIds),
|
||||
);
|
||||
|
||||
ids.forEach((x) => {
|
||||
if (pre.every((y) => y !== x)) {
|
||||
pre.push(x);
|
||||
// Iterative BFS with a visited set so cycles in the upstream graph
|
||||
// (e.g. answer:0 ↔ exesql:0 in the v1 exesql.json fixture) cannot
|
||||
// recurse forever. The previous recursive implementation had no cycle
|
||||
// detection and would blow the stack on any cyclic dsl.
|
||||
const visited = new Set<string>(nodeIds);
|
||||
const result: string[] = [];
|
||||
let frontier = [...nodeIds];
|
||||
while (frontier.length) {
|
||||
const next: string[] = [];
|
||||
for (const nodeId of frontier) {
|
||||
const upstreamIds = edges
|
||||
.filter((x) => x.target === nodeId)
|
||||
.map((x) => x.source);
|
||||
for (const id of upstreamIds) {
|
||||
if (!visited.has(id)) {
|
||||
visited.add(id);
|
||||
result.push(id);
|
||||
next.push(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return pre;
|
||||
}, []);
|
||||
}
|
||||
frontier = next;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function filterChildNodeIds(nodes: BaseNode[], nodeId?: string) {
|
||||
|
||||
@@ -168,10 +168,52 @@ export const downloadJsonFile = async (
|
||||
data: Record<string, any>,
|
||||
fileName: string,
|
||||
) => {
|
||||
const blob = new Blob([JSON.stringify(data)], { type: FileMimeType.Json });
|
||||
// Pretty-print with 2-space indent + sort keys at every depth so
|
||||
// the downloaded file is human-readable AND byte-stable across
|
||||
// re-exports: a user exporting the same canvas twice gets the
|
||||
// exact same bytes (modulo round-trip edits), which makes the
|
||||
// file easy to diff in version control and easy to hand-edit.
|
||||
// Mirrors the `sort_keys=True` we apply to the testdata fixtures
|
||||
// fixtures, so an exported dsl imported in v2 mode and re-
|
||||
// exported stays identical at the byte level.
|
||||
//
|
||||
// JSON.stringify already leaves non-ASCII (e.g. the Chinese
|
||||
// prompts we store under `Browser.prompts`) un-escaped by
|
||||
// default, so no `ensure_ascii` toggle is needed.
|
||||
const blob = new Blob([JSON.stringify(sortKeysDeep(data), null, 2)], {
|
||||
type: FileMimeType.Json,
|
||||
});
|
||||
downloadFileFromBlob(blob, fileName);
|
||||
};
|
||||
|
||||
// sortKeysDeep returns a structural copy of `value` with every
|
||||
// plain-object key sorted alphabetically. Array element order is
|
||||
// preserved (semantic — nodes/edges are not interchangeable), but
|
||||
// each element is recursively sorted if it is itself an object.
|
||||
// Primitives, `null`, and non-plain objects are passed through
|
||||
// unchanged. Used to make the exported dsl byte-stable: JSON
|
||||
// property iteration order is implementation-defined in JS, and
|
||||
// React Flow nodes carry fields in a stable order today but we
|
||||
// don't want to depend on that.
|
||||
const sortKeysDeep = (value: any): any => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(sortKeysDeep);
|
||||
}
|
||||
if (value !== null && typeof value === 'object') {
|
||||
// Only sort plain {…} objects — skip Date, RegExp, Map, etc.
|
||||
if (Object.getPrototypeOf(value) !== Object.prototype) {
|
||||
return value;
|
||||
}
|
||||
return Object.keys(value)
|
||||
.sort()
|
||||
.reduce<Record<string, any>>((acc, key) => {
|
||||
acc[key] = sortKeysDeep(value[key]);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export function transformBase64ToFileWithPreview(
|
||||
dataUrl: string,
|
||||
filename: string = 'file',
|
||||
|
||||
Reference in New Issue
Block a user