打开沙箱错误修改
This commit is contained in:
1 parent
ae184f434d
commit
d9844e0180
2 files changed
+267
-47
No files matched your search
@@ -1,3 +1,4 @@
|
|||||||
|
import type { BaseStream } from "@langchain/langgraph-sdk/react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Bot, FileText, LockKeyhole } from "lucide-react";
|
import { Bot, FileText, LockKeyhole } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -9,6 +10,7 @@ import { useArtifactsOptional } from "@/components/workspace/artifacts";
|
|||||||
import { useAgent } from "@/core/agents";
|
import { useAgent } from "@/core/agents";
|
||||||
import { useLocalSettings } from "@/core/settings";
|
import { useLocalSettings } from "@/core/settings";
|
||||||
import { useThreadStream } from "@/core/threads/hooks";
|
import { useThreadStream } from "@/core/threads/hooks";
|
||||||
|
import type { AgentThreadState } from "@/core/threads/types";
|
||||||
import { textOfMessage } from "@/core/threads/utils";
|
import { textOfMessage } from "@/core/threads/utils";
|
||||||
import { uuid } from "@/core/utils/uuid";
|
import { uuid } from "@/core/utils/uuid";
|
||||||
import {
|
import {
|
||||||
@@ -37,6 +39,11 @@ interface PositionNodeChatProps {
|
|||||||
path: string;
|
path: string;
|
||||||
name: string;
|
name: string;
|
||||||
mimeType: string | null;
|
mimeType: string | null;
|
||||||
|
auto?: boolean;
|
||||||
|
}) => void;
|
||||||
|
onThreadSnapshot?: (snapshot: {
|
||||||
|
threadId: string;
|
||||||
|
thread: BaseStream<AgentThreadState>;
|
||||||
}) => void;
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +91,53 @@ function hasMarkdownArtifact(manifest: Array<Record<string, unknown>>): boolean
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function artifactDisplayName(pathOrUrl: string): string {
|
||||||
|
let path = pathOrUrl;
|
||||||
|
if (pathOrUrl.startsWith("write-file:")) {
|
||||||
|
try {
|
||||||
|
path = decodeURIComponent(new URL(pathOrUrl).pathname);
|
||||||
|
} catch {
|
||||||
|
path = pathOrUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path.split("/").filter(Boolean).at(-1) || "Markdown 报告";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWriteFileVirtualUrl(path: string, messageId: string, toolCallId: string): string {
|
||||||
|
return new URL(
|
||||||
|
`write-file:${path}?message_id=${encodeURIComponent(
|
||||||
|
messageId,
|
||||||
|
)}&tool_call_id=${encodeURIComponent(toolCallId)}`,
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function latestStreamingWriteFile(
|
||||||
|
messages: AgentThreadState["messages"],
|
||||||
|
): { path: string; name: string; mimeType: string | null } | null {
|
||||||
|
for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
|
||||||
|
const message = messages[messageIndex] as (typeof messages)[number] & {
|
||||||
|
id?: string;
|
||||||
|
tool_calls?: Array<{ id?: string; name?: string; args?: Record<string, unknown> }>;
|
||||||
|
};
|
||||||
|
if (message.type !== "ai" || !message.id || !Array.isArray(message.tool_calls)) continue;
|
||||||
|
for (let toolIndex = message.tool_calls.length - 1; toolIndex >= 0; toolIndex -= 1) {
|
||||||
|
const toolCall = message.tool_calls[toolIndex];
|
||||||
|
if (toolCall?.name !== "write_file" || !toolCall.id) continue;
|
||||||
|
const rawPath = toolCall.args?.path;
|
||||||
|
if (typeof rawPath !== "string" || !rawPath.trim()) continue;
|
||||||
|
const content = toolCall.args?.content;
|
||||||
|
if (typeof content !== "string" || content.length === 0) continue;
|
||||||
|
const virtualPath = buildWriteFileVirtualUrl(rawPath, message.id, toolCall.id);
|
||||||
|
return {
|
||||||
|
path: virtualPath,
|
||||||
|
name: artifactDisplayName(rawPath),
|
||||||
|
mimeType: rawPath.toLowerCase().endsWith(".md") ? "text/markdown" : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Direct single-agent chat for one frozen business-chain node.
|
* Direct single-agent chat for one frozen business-chain node.
|
||||||
*
|
*
|
||||||
@@ -102,6 +156,7 @@ export function PositionNodeChat({
|
|||||||
onNodeUpdate,
|
onNodeUpdate,
|
||||||
onConversationStart,
|
onConversationStart,
|
||||||
onPreviewArtifact,
|
onPreviewArtifact,
|
||||||
|
onThreadSnapshot,
|
||||||
}: PositionNodeChatProps) {
|
}: PositionNodeChatProps) {
|
||||||
const [settings, setSettings] = useLocalSettings();
|
const [settings, setSettings] = useLocalSettings();
|
||||||
const { agent } = useAgent(node.agentId);
|
const { agent } = useAgent(node.agentId);
|
||||||
@@ -111,6 +166,12 @@ export function PositionNodeChat({
|
|||||||
const boundThreadIdRef = useRef<string | null>(node.threadId);
|
const boundThreadIdRef = useRef<string | null>(node.threadId);
|
||||||
const draftThreadIdRef = useRef(node.threadId ?? uuid());
|
const draftThreadIdRef = useRef(node.threadId ?? uuid());
|
||||||
const upstreamRevisionRef = useRef<Record<string, unknown> | null>(null);
|
const upstreamRevisionRef = useRef<Record<string, unknown> | null>(null);
|
||||||
|
// The parent opens the outer sandbox by updating the shared
|
||||||
|
// ArtifactsProvider. Forward that provider selection only once per file and
|
||||||
|
// thread: opening the sandbox itself changes the provider value, which used
|
||||||
|
// to re-run this effect and re-open the same sandbox indefinitely.
|
||||||
|
const forwardedArtifactKeyRef = useRef<string | null>(null);
|
||||||
|
const autoPreviewedArtifactKeyRef = useRef<string | null>(null);
|
||||||
const nodeArtifacts = useMemo(() => readNodeArtifacts(node.artifactManifest), [node.artifactManifest]);
|
const nodeArtifacts = useMemo(() => readNodeArtifacts(node.artifactManifest), [node.artifactManifest]);
|
||||||
const canWrite =
|
const canWrite =
|
||||||
sessionStatus !== "archived" &&
|
sessionStatus !== "archived" &&
|
||||||
@@ -120,16 +181,28 @@ export function PositionNodeChat({
|
|||||||
!node.invalidatedBy;
|
!node.invalidatedBy;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (artifactsCtx?.open && artifactsCtx.selectedArtifact) {
|
const selectedArtifact = artifactsCtx?.open
|
||||||
onPreviewArtifact?.({
|
? artifactsCtx.selectedArtifact
|
||||||
threadId: boundThreadIdRef.current ?? boundThreadId ?? draftThreadIdRef.current,
|
: null;
|
||||||
path: artifactsCtx.selectedArtifact,
|
if (!selectedArtifact) {
|
||||||
name: artifactsCtx.selectedArtifact.split("/").filter(Boolean).at(-1) || "Markdown 报告",
|
forwardedArtifactKeyRef.current = null;
|
||||||
mimeType: artifactsCtx.selectedArtifact.toLowerCase().endsWith(".md")
|
return;
|
||||||
? "text/markdown"
|
|
||||||
: null,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const threadId =
|
||||||
|
boundThreadIdRef.current ?? boundThreadId ?? draftThreadIdRef.current;
|
||||||
|
const artifactKey = `${threadId}:${selectedArtifact}`;
|
||||||
|
if (forwardedArtifactKeyRef.current === artifactKey) return;
|
||||||
|
forwardedArtifactKeyRef.current = artifactKey;
|
||||||
|
|
||||||
|
onPreviewArtifact?.({
|
||||||
|
threadId,
|
||||||
|
path: selectedArtifact,
|
||||||
|
name: selectedArtifact.split("/").filter(Boolean).at(-1) || "Markdown 报告",
|
||||||
|
mimeType: selectedArtifact.toLowerCase().endsWith(".md")
|
||||||
|
? "text/markdown"
|
||||||
|
: null,
|
||||||
|
});
|
||||||
}, [artifactsCtx?.open, artifactsCtx?.selectedArtifact, boundThreadId, onPreviewArtifact]);
|
}, [artifactsCtx?.open, artifactsCtx?.selectedArtifact, boundThreadId, onPreviewArtifact]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -209,6 +282,32 @@ export function PositionNodeChat({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const activeThreadId =
|
||||||
|
boundThreadIdRef.current ?? actualThreadId ?? boundThreadId ?? draftThreadIdRef.current;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onThreadSnapshot?.({ threadId: activeThreadId, thread });
|
||||||
|
}, [activeThreadId, onThreadSnapshot, thread]);
|
||||||
|
|
||||||
|
const streamingArtifact = useMemo(
|
||||||
|
() => (thread.isLoading ? latestStreamingWriteFile(thread.messages) : null),
|
||||||
|
[thread.isLoading, thread.messages],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!thread.isLoading || !streamingArtifact) return;
|
||||||
|
const artifactKey = `${activeThreadId}:${streamingArtifact.path}`;
|
||||||
|
if (autoPreviewedArtifactKeyRef.current === artifactKey) return;
|
||||||
|
autoPreviewedArtifactKeyRef.current = artifactKey;
|
||||||
|
onPreviewArtifact?.({
|
||||||
|
threadId: activeThreadId,
|
||||||
|
path: streamingArtifact.path,
|
||||||
|
name: streamingArtifact.name,
|
||||||
|
mimeType: streamingArtifact.mimeType,
|
||||||
|
auto: true,
|
||||||
|
});
|
||||||
|
}, [activeThreadId, onPreviewArtifact, streamingArtifact, thread.isLoading]);
|
||||||
|
|
||||||
// SDK thread creation is asynchronous. ``onStart`` normally binds it, but
|
// SDK thread creation is asynchronous. ``onStart`` normally binds it, but
|
||||||
// watching the actual stream id as well closes the last race where a page
|
// watching the actual stream id as well closes the last race where a page
|
||||||
// refresh could happen between creation and the async node-index write.
|
// refresh could happen between creation and the async node-index write.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { BaseStream } from "@langchain/langgraph-sdk/react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
@@ -40,6 +41,7 @@ import { agentColor } from "@/components/workspace/agent-icon-color";
|
|||||||
import { AgentDetailPanel } from "@/components/workspace/agents/agent-detail-panel";
|
import { AgentDetailPanel } from "@/components/workspace/agents/agent-detail-panel";
|
||||||
import { useAgentModuleSidebarData } from "@/components/workspace/agents/agent-module-sidebar";
|
import { useAgentModuleSidebarData } from "@/components/workspace/agents/agent-module-sidebar";
|
||||||
import { ArtifactFileDetail, useArtifactsOptional } from "@/components/workspace/artifacts";
|
import { ArtifactFileDetail, useArtifactsOptional } from "@/components/workspace/artifacts";
|
||||||
|
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -112,8 +114,37 @@ type PositionArtifactPreview = {
|
|||||||
path: string;
|
path: string;
|
||||||
name: string;
|
name: string;
|
||||||
mimeType: string | null;
|
mimeType: string | null;
|
||||||
|
auto?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function sandboxThreadSignature(thread: BaseStream<AgentThreadState>): string {
|
||||||
|
const messages = thread.messages ?? [];
|
||||||
|
for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
|
||||||
|
const message = messages[messageIndex] as (typeof messages)[number] & {
|
||||||
|
id?: string;
|
||||||
|
tool_calls?: Array<{ id?: string; name?: string; args?: Record<string, unknown> }>;
|
||||||
|
};
|
||||||
|
if (message.type !== "ai" || !Array.isArray(message.tool_calls)) continue;
|
||||||
|
for (let toolIndex = message.tool_calls.length - 1; toolIndex >= 0; toolIndex -= 1) {
|
||||||
|
const toolCall = message.tool_calls[toolIndex];
|
||||||
|
if (toolCall?.name !== "write_file") continue;
|
||||||
|
const path = typeof toolCall.args?.path === "string" ? toolCall.args.path : "";
|
||||||
|
const content = typeof toolCall.args?.content === "string" ? toolCall.args.content : "";
|
||||||
|
return [
|
||||||
|
thread.isLoading ? "1" : "0",
|
||||||
|
messages.length,
|
||||||
|
message.id ?? "",
|
||||||
|
toolCall.id ?? "",
|
||||||
|
path,
|
||||||
|
content.length,
|
||||||
|
content.slice(-32),
|
||||||
|
].join("|");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const lastMessage = messages.at(-1) as (typeof messages)[number] & { id?: string } | undefined;
|
||||||
|
return [thread.isLoading ? "1" : "0", messages.length, lastMessage?.id ?? ""].join("|");
|
||||||
|
}
|
||||||
|
|
||||||
function uniqueChains(chains: BusinessChain[]): BusinessChain[] {
|
function uniqueChains(chains: BusinessChain[]): BusinessChain[] {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
return chains.filter((chain) => {
|
return chains.filter((chain) => {
|
||||||
@@ -389,8 +420,22 @@ export default function PositionRoundtablePage({
|
|||||||
const [rightPanelOpen, setRightPanelOpen] = useState(true);
|
const [rightPanelOpen, setRightPanelOpen] = useState(true);
|
||||||
const [rightTab, setRightTab] = useState<WorkspaceTab>("agents");
|
const [rightTab, setRightTab] = useState<WorkspaceTab>("agents");
|
||||||
const artifactsCtx = useArtifactsOptional();
|
const artifactsCtx = useArtifactsOptional();
|
||||||
|
const artifactsCtxRef = useRef(artifactsCtx);
|
||||||
const [sandboxArtifact, setSandboxArtifact] = useState<PositionArtifactPreview | null>(null);
|
const [sandboxArtifact, setSandboxArtifact] = useState<PositionArtifactPreview | null>(null);
|
||||||
|
const sandboxArtifactRef = useRef<PositionArtifactPreview | null>(null);
|
||||||
|
const closedSandboxKeyRef = useRef<string | null>(null);
|
||||||
|
const [sandboxThreadSnapshot, setSandboxThreadSnapshot] = useState<{
|
||||||
|
threadId: string;
|
||||||
|
thread: BaseStream<AgentThreadState>;
|
||||||
|
signature: string;
|
||||||
|
} | null>(null);
|
||||||
const activePosition = POSITIONS.find((item) => item.id === positionId)!;
|
const activePosition = POSITIONS.find((item) => item.id === positionId)!;
|
||||||
|
useEffect(() => {
|
||||||
|
artifactsCtxRef.current = artifactsCtx;
|
||||||
|
}, [artifactsCtx]);
|
||||||
|
useEffect(() => {
|
||||||
|
sandboxArtifactRef.current = sandboxArtifact;
|
||||||
|
}, [sandboxArtifact]);
|
||||||
const frozenIntent = useMemo(
|
const frozenIntent = useMemo(
|
||||||
() => parseIntentSummary(positionSession?.intentSnapshot),
|
() => parseIntentSummary(positionSession?.intentSnapshot),
|
||||||
[positionSession?.intentSnapshot],
|
[positionSession?.intentSnapshot],
|
||||||
@@ -784,6 +829,13 @@ export default function PositionRoundtablePage({
|
|||||||
}, [intent.intentReady, intent.intentThreadId, positionSession, searchParams, selectedChain, setSearchParams, startingChain, task]);
|
}, [intent.intentReady, intent.intentThreadId, positionSession, searchParams, selectedChain, setSearchParams, startingChain, task]);
|
||||||
|
|
||||||
const handleCreateNewSession = useCallback(() => {
|
const handleCreateNewSession = useCallback(() => {
|
||||||
|
const ctx = artifactsCtxRef.current;
|
||||||
|
if (sandboxArtifactRef.current || ctx?.open) {
|
||||||
|
ctx?.setOpen(false);
|
||||||
|
}
|
||||||
|
closedSandboxKeyRef.current = null;
|
||||||
|
setSandboxArtifact(null);
|
||||||
|
setSandboxThreadSnapshot(null);
|
||||||
intent.resetIntent();
|
intent.resetIntent();
|
||||||
hydratedIntentSessionRef.current = null;
|
hydratedIntentSessionRef.current = null;
|
||||||
persistedIntentRef.current = null;
|
persistedIntentRef.current = null;
|
||||||
@@ -812,29 +864,88 @@ export default function PositionRoundtablePage({
|
|||||||
});
|
});
|
||||||
}, [positionSession]);
|
}, [positionSession]);
|
||||||
|
|
||||||
const handlePreviewArtifact = useCallback(
|
const handlePreviewArtifact = useCallback((artifact: PositionArtifactPreview) => {
|
||||||
(artifact: PositionArtifactPreview) => {
|
if (!artifact.threadId || !artifact.path) {
|
||||||
if (!artifact.threadId || !artifact.path) {
|
toast.warning("该文件缺少沙箱预览所需的信息");
|
||||||
toast.warning("该文件缺少沙箱预览所需的信息");
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
// 与普通智能体问答完全使用同一套 ArtifactsProvider:选中文件、展开
|
const artifactKey = `${artifact.threadId}:${artifact.path}`;
|
||||||
// 文档面板,并让 ArtifactFileDetail 以所属 thread 读取沙箱文件。
|
if (artifact.auto && closedSandboxKeyRef.current === artifactKey) {
|
||||||
setSandboxArtifact(artifact);
|
return;
|
||||||
artifactsCtx?.setArtifacts([artifact.path]);
|
}
|
||||||
artifactsCtx?.select(artifact.path);
|
closedSandboxKeyRef.current = null;
|
||||||
artifactsCtx?.setOpen(true);
|
|
||||||
setRightPanelOpen(false);
|
setSandboxArtifact((current) =>
|
||||||
},
|
current?.threadId === artifact.threadId && current.path === artifact.path
|
||||||
[artifactsCtx],
|
? current
|
||||||
);
|
: artifact,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ctx = artifactsCtxRef.current;
|
||||||
|
if (ctx?.selectedArtifact !== artifact.path) {
|
||||||
|
ctx?.setArtifacts([artifact.path]);
|
||||||
|
ctx?.select(artifact.path, artifact.auto ?? false);
|
||||||
|
}
|
||||||
|
if (!ctx?.open) {
|
||||||
|
ctx?.setOpen(true);
|
||||||
|
}
|
||||||
|
setRightPanelOpen(false);
|
||||||
|
}, []);
|
||||||
const sandboxOpen = Boolean(
|
const sandboxOpen = Boolean(
|
||||||
sandboxArtifact && artifactsCtx?.open && artifactsCtx.selectedArtifact,
|
sandboxArtifact && artifactsCtx?.open && artifactsCtx.selectedArtifact,
|
||||||
);
|
);
|
||||||
|
// ArtifactFileDetail is shared with the normal LangGraph chat surface and
|
||||||
|
// therefore reads ThreadContext for streaming state. The position workspace
|
||||||
|
// renders its sandbox as a sibling of PositionNodeChat, so it is outside the
|
||||||
|
// node's provider. Artifact files here always load through the artifacts API;
|
||||||
|
// provide the smallest stable stream shape needed by the preview component.
|
||||||
|
const sandboxThreadStub = useMemo(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
messages: [],
|
||||||
|
isLoading: false,
|
||||||
|
isThreadLoading: false,
|
||||||
|
values: {
|
||||||
|
title: "",
|
||||||
|
messages: [],
|
||||||
|
artifacts: artifactsCtx?.artifacts ?? [],
|
||||||
|
},
|
||||||
|
}) as unknown as BaseStream<AgentThreadState>,
|
||||||
|
[artifactsCtx?.artifacts],
|
||||||
|
);
|
||||||
|
const sandboxThread = useMemo(
|
||||||
|
() =>
|
||||||
|
sandboxArtifact &&
|
||||||
|
sandboxThreadSnapshot?.threadId === sandboxArtifact.threadId
|
||||||
|
? sandboxThreadSnapshot.thread
|
||||||
|
: sandboxThreadStub,
|
||||||
|
[sandboxArtifact, sandboxThreadSnapshot, sandboxThreadStub],
|
||||||
|
);
|
||||||
|
const handleNodeThreadSnapshot = useCallback(
|
||||||
|
(snapshot: { threadId: string; thread: BaseStream<AgentThreadState> }) => {
|
||||||
|
const signature = sandboxThreadSignature(snapshot.thread);
|
||||||
|
setSandboxThreadSnapshot((current) => {
|
||||||
|
const sameThreadData =
|
||||||
|
current?.threadId === snapshot.threadId &&
|
||||||
|
current.signature === signature;
|
||||||
|
return sameThreadData ? current : { ...snapshot, signature };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
const closeSandbox = useCallback(() => {
|
const closeSandbox = useCallback(() => {
|
||||||
artifactsCtx?.setOpen(false);
|
const current = sandboxArtifactRef.current;
|
||||||
|
if (current) {
|
||||||
|
closedSandboxKeyRef.current = `${current.threadId}:${current.path}`;
|
||||||
|
}
|
||||||
|
const ctx = artifactsCtxRef.current;
|
||||||
|
if (current || ctx?.open) {
|
||||||
|
ctx?.setOpen(false);
|
||||||
|
}
|
||||||
setSandboxArtifact(null);
|
setSandboxArtifact(null);
|
||||||
}, [artifactsCtx]);
|
setSandboxThreadSnapshot(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -911,6 +1022,7 @@ export default function PositionRoundtablePage({
|
|||||||
// 其他岗位改用按需打开的抽屉,避免一进岗位就挤占对话空间。
|
// 其他岗位改用按需打开的抽屉,避免一进岗位就挤占对话空间。
|
||||||
const nextPositionId = String(value) as PositionId;
|
const nextPositionId = String(value) as PositionId;
|
||||||
// 中间区由 activeNode 的兜底规则自动定位到首个流程节点。
|
// 中间区由 activeNode 的兜底规则自动定位到首个流程节点。
|
||||||
|
closeSandbox();
|
||||||
setPositionId(nextPositionId);
|
setPositionId(nextPositionId);
|
||||||
setSelectedAgentId(null);
|
setSelectedAgentId(null);
|
||||||
setActiveNodeKey(null);
|
setActiveNodeKey(null);
|
||||||
@@ -918,10 +1030,10 @@ export default function PositionRoundtablePage({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{compactChrome && (
|
{compactChrome && (
|
||||||
<>
|
<div className="contents">
|
||||||
<ThemeSwitcher tone="content" />
|
<ThemeSwitcher tone="content" />
|
||||||
<LayoutModeSwitcher tone="content" />
|
<LayoutModeSwitcher tone="content" />
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -943,7 +1055,7 @@ export default function PositionRoundtablePage({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="contents">
|
||||||
<IntentUnderstandingCard intent={intent.intentReady} />
|
<IntentUnderstandingCard intent={intent.intentReady} />
|
||||||
<BusinessChainFlow
|
<BusinessChainFlow
|
||||||
chain={activeChain}
|
chain={activeChain}
|
||||||
@@ -951,6 +1063,7 @@ export default function PositionRoundtablePage({
|
|||||||
session={positionSession}
|
session={positionSession}
|
||||||
activeNodeKey={activeNodeKey}
|
activeNodeKey={activeNodeKey}
|
||||||
onSelectNode={(node) => {
|
onSelectNode={(node) => {
|
||||||
|
closeSandbox();
|
||||||
setActiveNodeKey(node.nodeKey);
|
setActiveNodeKey(node.nodeKey);
|
||||||
setSelectedAgentId(node.agentId);
|
setSelectedAgentId(node.agentId);
|
||||||
}}
|
}}
|
||||||
@@ -959,7 +1072,7 @@ export default function PositionRoundtablePage({
|
|||||||
setRightTab("results");
|
setRightTab("results");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -988,7 +1101,7 @@ export default function PositionRoundtablePage({
|
|||||||
positionSession?.status !== "intent_pending" && frozenIntent ? (
|
positionSession?.status !== "intent_pending" && frozenIntent ? (
|
||||||
<IntentConfirmedState intent={frozenIntent} />
|
<IntentConfirmedState intent={frozenIntent} />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="contents">
|
||||||
{intent.intentInitError && (
|
{intent.intentInitError && (
|
||||||
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-amber-200 bg-amber-50 px-4 py-2 text-xs text-amber-800 dark:border-amber-900/60 dark:bg-amber-950/30 dark:text-amber-200">
|
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-amber-200 bg-amber-50 px-4 py-2 text-xs text-amber-800 dark:border-amber-900/60 dark:bg-amber-950/30 dark:text-amber-200">
|
||||||
<span className="line-clamp-2">对话初始化失败:{intent.intentInitError}</span>
|
<span className="line-clamp-2">对话初始化失败:{intent.intentInitError}</span>
|
||||||
@@ -1035,7 +1148,7 @@ export default function PositionRoundtablePage({
|
|||||||
{startingChain ? "正在保存意图并生成岗位节点…" : "正在恢复岗位协同会话…"}
|
{startingChain ? "正在保存意图并生成岗位节点…" : "正在恢复岗位协同会话…"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
) : positionSession && activeNode ? (
|
) : positionSession && activeNode ? (
|
||||||
<PositionNodeChat
|
<PositionNodeChat
|
||||||
@@ -1050,6 +1163,7 @@ export default function PositionRoundtablePage({
|
|||||||
onNodeUpdate={handleNodeUpdate}
|
onNodeUpdate={handleNodeUpdate}
|
||||||
onConversationStart={() => setRightPanelOpen(false)}
|
onConversationStart={() => setRightPanelOpen(false)}
|
||||||
onPreviewArtifact={handlePreviewArtifact}
|
onPreviewArtifact={handlePreviewArtifact}
|
||||||
|
onThreadSnapshot={handleNodeThreadSnapshot}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PositionNodeEmptyState
|
<PositionNodeEmptyState
|
||||||
@@ -1068,6 +1182,7 @@ export default function PositionRoundtablePage({
|
|||||||
session={positionSession}
|
session={positionSession}
|
||||||
activeNodeKey={activeNodeKey}
|
activeNodeKey={activeNodeKey}
|
||||||
onSelectNode={(node) => {
|
onSelectNode={(node) => {
|
||||||
|
closeSandbox();
|
||||||
setActiveNodeKey(node.nodeKey);
|
setActiveNodeKey(node.nodeKey);
|
||||||
setSelectedAgentId(node.agentId);
|
setSelectedAgentId(node.agentId);
|
||||||
}}
|
}}
|
||||||
@@ -1096,6 +1211,7 @@ export default function PositionRoundtablePage({
|
|||||||
nodes={positionSession?.nodes ?? []}
|
nodes={positionSession?.nodes ?? []}
|
||||||
chain={activeChain}
|
chain={activeChain}
|
||||||
onSelectNode={(node) => {
|
onSelectNode={(node) => {
|
||||||
|
closeSandbox();
|
||||||
setActiveNodeKey(node.nodeKey);
|
setActiveNodeKey(node.nodeKey);
|
||||||
setSelectedAgentId(node.agentId);
|
setSelectedAgentId(node.agentId);
|
||||||
}}
|
}}
|
||||||
@@ -1138,6 +1254,7 @@ export default function PositionRoundtablePage({
|
|||||||
nodes={positionSession?.nodes ?? []}
|
nodes={positionSession?.nodes ?? []}
|
||||||
chain={activeChain}
|
chain={activeChain}
|
||||||
onSelectNode={(node) => {
|
onSelectNode={(node) => {
|
||||||
|
closeSandbox();
|
||||||
setActiveNodeKey(node.nodeKey);
|
setActiveNodeKey(node.nodeKey);
|
||||||
setSelectedAgentId(node.agentId);
|
setSelectedAgentId(node.agentId);
|
||||||
}}
|
}}
|
||||||
@@ -1153,7 +1270,7 @@ export default function PositionRoundtablePage({
|
|||||||
)}
|
)}
|
||||||
{sandboxOpen && sandboxArtifact && artifactsCtx?.selectedArtifact && (
|
{sandboxOpen && sandboxArtifact && artifactsCtx?.selectedArtifact && (
|
||||||
<aside className="flex w-[min(48vw,720px)] min-w-[440px] shrink-0 flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80 dark:shadow-none">
|
<aside className="flex w-[min(48vw,720px)] min-w-[440px] shrink-0 flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80 dark:shadow-none">
|
||||||
<header className="flex h-12 shrink-0 items-center justify-between gap-3 border-b border-slate-200 px-4 dark:border-[#2b8ffe]/25">
|
<header className="hidden h-12 shrink-0 items-center justify-between gap-3 border-b border-slate-200 px-4 dark:border-[#2b8ffe]/25">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate text-sm font-bold text-slate-800 dark:text-[#e8edf5]">
|
<p className="truncate text-sm font-bold text-slate-800 dark:text-[#e8edf5]">
|
||||||
{desensitize(sandboxArtifact.name)}
|
{desensitize(sandboxArtifact.name)}
|
||||||
@@ -1170,11 +1287,15 @@ export default function PositionRoundtablePage({
|
|||||||
title="关闭沙箱"
|
title="关闭沙箱"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<ArtifactFileDetail
|
<ThreadContext.Provider value={{ thread: sandboxThread, isMock: false }}>
|
||||||
className="min-h-0 flex-1"
|
<ArtifactFileDetail
|
||||||
filepath={artifactsCtx.selectedArtifact}
|
className="min-h-0 flex-1"
|
||||||
threadId={sandboxArtifact.threadId}
|
filepath={sandboxArtifact.path}
|
||||||
/>
|
threadId={sandboxArtifact.threadId}
|
||||||
|
onClose={closeSandbox}
|
||||||
|
disableDesensitize
|
||||||
|
/>
|
||||||
|
</ThreadContext.Provider>
|
||||||
</aside>
|
</aside>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1890,7 +2011,7 @@ function BusinessChainFlow({
|
|||||||
请先由情报分析岗选择业务链条
|
请先由情报分析岗选择业务链条
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="contents">
|
||||||
<ChainFlowCanvas
|
<ChainFlowCanvas
|
||||||
chain={chain}
|
chain={chain}
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@@ -1928,7 +2049,7 @@ function BusinessChainFlow({
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@@ -2911,7 +3032,7 @@ function AnalysisResults({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="contents">
|
||||||
<div className="space-y-3 rounded-xl border border-slate-200 p-3.5 dark:border-border">
|
<div className="space-y-3 rounded-xl border border-slate-200 p-3.5 dark:border-border">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -3009,7 +3130,7 @@ function AnalysisResults({
|
|||||||
const status = item.status ? nodeStatusMeta(item.status) : null;
|
const status = item.status ? nodeStatusMeta(item.status) : null;
|
||||||
const StatusIcon = status?.Icon;
|
const StatusIcon = status?.Icon;
|
||||||
const contents = (
|
const contents = (
|
||||||
<>
|
<div className="contents">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -3064,7 +3185,7 @@ function AnalysisResults({
|
|||||||
{displayedTime}
|
{displayedTime}
|
||||||
</span>
|
</span>
|
||||||
</footer>
|
</footer>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
return item.node ? (
|
return item.node ? (
|
||||||
<button
|
<button
|
||||||
@@ -3132,7 +3253,7 @@ function AnalysisResults({
|
|||||||
onReasonChange={setRejectReason}
|
onReasonChange={setRejectReason}
|
||||||
onSubmit={() => void handleRejectSubmit()}
|
onSubmit={() => void handleRejectSubmit()}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3210,14 +3331,14 @@ function DeliverableDetailDialog({
|
|||||||
时间:{displayedTime || "暂无"}
|
时间:{displayedTime || "暂无"}
|
||||||
</div>
|
</div>
|
||||||
{item.node && (
|
{item.node && (
|
||||||
<>
|
<div className="contents">
|
||||||
<div className="rounded-lg bg-slate-50 px-3 py-2 dark:bg-[#032650]">
|
<div className="rounded-lg bg-slate-50 px-3 py-2 dark:bg-[#032650]">
|
||||||
版本:第 {item.node.revision} 版
|
版本:第 {item.node.revision} 版
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg bg-slate-50 px-3 py-2 dark:bg-[#032650]">
|
<div className="rounded-lg bg-slate-50 px-3 py-2 dark:bg-[#032650]">
|
||||||
驳回次数:{item.node.rejectionCount}
|
驳回次数:{item.node.rejectionCount}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user