feat(runtime): 即梦本机运行时 - 接入安全生成闭环与桌面本机渠道

Refs #225

Co-authored-by: zkhyww <127009345+zkhyww@users.noreply.github.com>
This commit is contained in:
ddCatandzkhyww committed 2026-08-15 12:45:29 +08:00
1 parent 8f2232feee
commit 57e1820f41
179 files changed
+49746 -1897

No files matched your search

+86 -11
View File
@@ -4,6 +4,9 @@ import { useQuery } from "@tanstack/react-query";
import { useParams, useSearchParams } from "react-router";
import { useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
import { uploadMediaFile } from "@/services/file-storage";
import { readLocalRuntimeBootstrapState } from "@/services/local-runtime-bootstrap";
import { createCanvasGenerationLiveProjectAdapter, registerCanvasGenerationLiveProject } from "@/services/canvas-generation-consumer";
import { getActiveUserScope } from "@/lib/user-scope";
import { resourceFileUrl, resourceIdFromStorageKey } from "@/services/api/resources";
import copyToClipboard from "copy-to-clipboard";
import { nanoid } from "nanoid";
@@ -11,7 +14,9 @@ import { canvasThemes, type CanvasBackgroundMode } from "@/lib/canvas-theme";
import { persistCanvasMediaPerformanceMode, readCanvasMediaPerformanceMode } from "@/lib/canvas/canvas-performance-mode";
import { summarizeCanvasContext } from "@/lib/canvas/canvas-context-summary";
import { refreshCanvasCharacterReferenceNodes } from "@/lib/canvas/canvas-character-reference";
import { shouldAutoConnectCanvasRuntime } from "@/lib/canvas/local-runtime-connection";
import { useAssetStore } from "@/stores/use-asset-store";
import { flushCanvasStorePersistence } from "@/stores/canvas/use-canvas-store";
import { useThemeStore } from "@/stores/use-theme-store";
import { useUserStore } from "@/stores/use-user-store";
import { App } from "antd";
@@ -60,6 +65,7 @@ import { CanvasConnectionCreateMenu, CanvasNodePanelOverlay } from "@/components
import { CanvasLeaferGraphicsLayer } from "@/components/canvas/canvas-leafer-graphics-layer";
import { CanvasFreeformEmptyState, CanvasLinkedProjectEmptyState, CanvasShortDramaEmptyState, CanvasShortDramaGuide, CanvasStoryInputNodeContent, CanvasStylePlaceholderNodeContent } from "@/components/canvas/canvas-short-drama-entry";
import { createCanvasNode, getInputSummary, isHiddenBatchChild, persistCanvasWorkspaceMode, readCanvasWorkspaceMode } from "@/lib/canvas/canvas-project-domain";
import { canvasAssetHandoffAttempt, finalizeCanvasAssetHandoff, uninsertedCanvasAssetHandoffPayloads } from "@/lib/canvas/canvas-asset-handoff";
import { deriveStoryboardPipelineProgress } from "@/lib/canvas/canvas-storyboard-progress";
import { CanvasAgentChangeToast, CanvasMergeStatusToast, CanvasUploadStatusToast } from "./canvas-project-feedback";
import { backendProviderConfig, getGenerationCount } from "@/lib/canvas/canvas-project-generation";
@@ -82,7 +88,7 @@ import { useCanvasStyleWorkflow } from "./use-canvas-style-workflow";
import { useCanvasDirector } from "./use-canvas-director";
import { useCanvasGeneration } from "./use-canvas-generation";
import { useCanvasGenerationBatches } from "./use-canvas-generation-batches";
import { useCanvasGenerationExecutor } from "./use-canvas-generation-executor";
import { useCanvasGenerationExecutor, type CanvasNodeGenerationOptions } from "./use-canvas-generation-executor";
import { useCanvasGenerationRetry } from "./use-canvas-generation-retry";
import { useCanvasHistory } from "./use-canvas-history";
import { useCanvasKeyboard } from "./use-canvas-keyboard";
@@ -143,19 +149,22 @@ export default function CanvasPage() {
function InfiniteCanvasPage() {
const { message } = App.useApp();
const params = useParams<{ id: string }>();
const [searchParams] = useSearchParams();
const [searchParams, setSearchParams] = useSearchParams();
const projectId = params.id || "";
const canvasStorageScope = getActiveUserScope();
const localAgentConnected = useCanvasAgentStore((state) => state.connected);
const localAgentActivity = useCanvasAgentStore((state) => state.activity);
const localAgentEnabled = useCanvasAgentStore((state) => state.enabled);
const containerRef = useRef<HTMLDivElement>(null);
const didInitialCenterRef = useRef(false);
const toolbarHideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const assetHandoffRef = useRef("");
const config = useConfigStore((state) => state.config);
const effectiveConfig = useEffectiveConfig();
const isAiConfigReady = useConfigStore((state) => state.isAiConfigReady);
const assets = useAssetStore((state) => state.assets);
const assetsHydrated = useAssetStore((state) => state.hydrated);
const cleanupAssetImages = useAssetStore((state) => state.cleanupImages);
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const defaultDrawingEngine = useUserStore((state) => state.drawingEngine.defaultEngine);
@@ -199,8 +208,8 @@ function InfiniteCanvasPage() {
const [scriptScrollTopById, setScriptScrollTopById] = useState<Record<string, number>>({});
const [directorNodeId, setDirectorNodeId] = useState<string | null>(null);
const [versionCompareRootId, setVersionCompareRootId] = useState<string | null>(null);
const codexAutoConnect = ["new", "recent", "choose"].includes(searchParams.get("mode") || "");
const codexCompactAgent = codexAutoConnect && searchParams.has("agentUrl");
const codexAutoConnect = shouldAutoConnectCanvasRuntime(searchParams);
const codexCompactAgent = codexAutoConnect && readLocalRuntimeBootstrapState().legacyDeepLinkRejected;
const [titleEditing, setTitleEditing] = useState(false);
const [titleDraft, setTitleDraft] = useState("");
const [shortcutRequestNonce, setShortcutRequestNonce] = useState(0);
@@ -253,9 +262,20 @@ function InfiniteCanvasPage() {
const nodesRef = useRef(nodes);
const connectionsRef = useRef(connections);
const chatSessionsRef = useRef(chatSessions);
const activeChatIdRef = useRef(activeChatId);
const selectedNodeIdsRef = useRef(selectedNodeIds);
const viewportRef = useRef(viewport);
const generateNodeRef = useRef<((nodeId: string, mode: CanvasNodeGenerationMode, prompt: string) => Promise<void>) | null>(null);
const generateNodeRef = useRef<((nodeId: string, mode: CanvasNodeGenerationMode, prompt: string, options?: CanvasNodeGenerationOptions) => Promise<void>) | null>(null);
useEffect(() => {
if (!projectId) return;
return registerCanvasGenerationLiveProject({
scope: canvasStorageScope,
projectId,
adapter: createCanvasGenerationLiveProjectAdapter({ nodesRef, connectionsRef, chatSessionsRef, activeChatIdRef, setNodes, setConnections, setChatSessions, setActiveChatId }),
});
}, [canvasStorageScope, projectId]);
const { getHistoryCleanupContext, historyPausedRef, historyState, redoCanvas, resetHistory, undoCanvas } = useCanvasHistory({
projectLoaded,
@@ -318,17 +338,30 @@ function InfiniteCanvasPage() {
}, [linkedProjectQuery.data, projectLoaded, setNodes]);
const canvasContext = useMemo(() => summarizeCanvasContext(nodes, selectedNodeIds, linkedProjectQuery.data?.units), [linkedProjectQuery.data?.units, nodes, selectedNodeIds]);
const { bindGenerationTask, cancelNodeTask, confirmStopGeneration, finishGenerationRequest, openNodeTaskDetails, runningNodeId, setRunningNodeId, setTaskDetail, startGenerationRequest, taskDetail, taskDetailLoading, taskDetailLogs } =
useCanvasGeneration({ projectId, domainProjectId: linkedProjectId, projectLoaded, nodes, nodesRef, setNodes });
const {
applyGenerationTaskResult,
bindGenerationTask,
cancelNodeTask,
confirmStopGeneration,
finishGenerationRequest,
openNodeTaskDetails,
runningNodeId,
setRunningNodeId,
setTaskDetail,
startGenerationRequest,
taskDetail,
taskDetailLoading,
taskDetailLogs,
} = useCanvasGeneration({ projectId, domainProjectId: linkedProjectId, projectLoaded, nodes, nodesRef, setNodes });
useEffect(() => {
if (!projectLoaded || !["new", "recent", "choose"].includes(searchParams.get("mode") || "")) return;
if (searchParams.has("agentUrl")) {
if (!projectLoaded || !codexAutoConnect) return;
if (codexCompactAgent) {
setAgentMode("local");
return;
}
openAgent("local");
}, [openAgent, projectLoaded, searchParams, setAgentMode]);
}, [codexAutoConnect, codexCompactAgent, openAgent, projectLoaded, setAgentMode]);
// 沉浸专注进入时收起智能体与小地图、重置 Dock 唤出态;仅响应「进入」瞬间,避免关闭专注内主动唤出的面板。
const prevFocusModeRef = useRef(focusMode);
@@ -348,9 +381,11 @@ function InfiniteCanvasPage() {
useLayoutEffect(() => {
nodesRef.current = nodes;
connectionsRef.current = connections;
chatSessionsRef.current = chatSessions;
activeChatIdRef.current = activeChatId;
selectedNodeIdsRef.current = selectedNodeIds;
viewportRef.current = viewport;
}, [nodes, connections, selectedNodeIds, viewport]);
}, [activeChatId, chatSessions, nodes, connections, selectedNodeIds, viewport]);
useEffect(() => {
if (!projectLoaded) return;
@@ -477,6 +512,42 @@ function InfiniteCanvasPage() {
setDialogNodeId,
});
useEffect(() => {
if (!projectLoaded || !assetsHydrated || searchParams.get("mode") !== "handoff") return;
const attempt = canvasAssetHandoffAttempt(assets, searchParams);
const { assetIds, payloads } = attempt;
if (!assetIds.length) return;
const assetReadiness = assetIds
.map((assetId) => {
const asset = assets.find((candidate) => candidate.id === assetId);
return `${assetId}:${asset?.kind || "missing"}`;
})
.join("|");
const handoffKey = `${projectId}:${assetReadiness}`;
if (assetHandoffRef.current === handoffKey) return;
assetHandoffRef.current = handoffKey;
if (attempt.kind === "retry") return;
const pendingPayloads = uninsertedCanvasAssetHandoffPayloads(nodesRef.current, payloads);
const persistHandoff = async (createdNodes: CanvasNodeData[]) => {
const finalized = await finalizeCanvasAssetHandoff({
searchParams,
currentNodes: nodesRef.current,
createdNodes,
persist: async (nextNodes) => {
nodesRef.current = nextNodes;
updateProject(projectId, { nodes: nextNodes });
await flushCanvasStorePersistence();
},
});
setSearchParams(finalized.searchParams, { replace: true });
};
const insertion = pendingPayloads.length ? handleProjectAssetsInsert(pendingPayloads) : Promise.resolve([] as CanvasNodeData[]);
void insertion.then(persistHandoff).catch(() => {
assetHandoffRef.current = "";
});
}, [assets, assetsHydrated, handleProjectAssetsInsert, message, nodesRef, projectId, projectLoaded, searchParams, setSearchParams, updateProject]);
// 时间线弹窗内新增素材的回填通道:素材库/上传创建节点后由弹窗通过 ref 加入草稿。
const timelineAddNodeRef = useRef<((node: CanvasNodeData) => void) | null>(null);
// 时间线作用域直连媒体入轨通道:素材库/项目资产/本地上传不落画布,仅加入时间线草稿。
@@ -1063,6 +1134,8 @@ function InfiniteCanvasPage() {
});
const handleAssistantSessionsChange = useCallback((sessions: CanvasAssistantSession[], activeId: string | null) => {
chatSessionsRef.current = sessions;
activeChatIdRef.current = activeId;
setChatSessions(sessions);
setActiveChatId(activeId);
}, []);
@@ -1193,6 +1266,7 @@ function InfiniteCanvasPage() {
startGenerationRequest,
finishGenerationRequest,
bindGenerationTask,
applyGenerationTaskResult,
});
useEffect(() => {
generateNodeRef.current = handleGenerateNode;
@@ -1230,6 +1304,7 @@ function InfiniteCanvasPage() {
startGenerationRequest,
finishGenerationRequest,
bindGenerationTask,
applyGenerationTaskResult,
});
const generateImageFromTextNode = useCallback(