Studio: 增量/覆盖保存、节点切换与绑定编辑弹窗

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 23:28:49 +08:00
parent 0f50c98cf3
commit 71e673a2ed
13 changed files with 1504 additions and 294 deletions

View File

@@ -6,7 +6,7 @@ import {
buildWorkflowVariableLabelMap,
getWorkflowVariableLabel,
} from './edit/workflowVariableLabels';
import { resolveBoundCharacterName, canUndoRun, canRerollRun, canAdvanceNextStep, getRunControls, hasRunControl } from './studioRunUtils';
import { resolveBoundCharacterName, canUndoRun, canRerollRun, canAdvanceNextStep, getRunControls, hasRunControl, shouldShowFirstExport, shouldShowSaveAppendOverwrite, canSaveWorldbookEntry, canSwitchToNode } from './studioRunUtils';
import useCharacterStore from '../../Store/SideBarLeft/CharacterSlice';
import StudioContextBlockPopup from './StudioContextBlockPopup';
@@ -499,6 +499,8 @@ function StudioRunPage() {
fetchProject,
selectRun,
advanceRun,
saveWorldbookRun,
switchRunNode,
sendRunMessage,
undoRun,
rerollRun,
@@ -735,10 +737,10 @@ function StudioRunPage() {
};
const handleNextStep = async () => {
if (!hasRunControl(activeNodeState?.skillId, 'nextStep', skillTemplates)) return;
if (runAdvancing || runMessaging || !canAdvanceNextStep(activeNodeState)) {
return;
}
clearConfirmPending();
const run = await advanceRun({});
if (run) {
setChatInput('');
@@ -746,6 +748,41 @@ function StudioRunPage() {
}
};
const handleSaveIncremental = async () => {
if (!hasRunControl(activeNodeState?.skillId, 'incrementalSave', skillTemplates)) return;
if (runAdvancing || runMessaging || !canSaveWorldbookEntry(activeNodeState)) return;
clearConfirmPending();
await saveWorldbookRun('incremental');
};
const handleSaveOverwrite = async () => {
if (!hasRunControl(activeNodeState?.skillId, 'overwriteSave', skillTemplates)) return;
if (runAdvancing || runMessaging || !canSaveWorldbookEntry(activeNodeState)) return;
clearConfirmPending();
await saveWorldbookRun('overwrite');
};
const handleSaveIncrementalClick = () => {
if (runAdvancing || runMessaging || !canSaveWorldbookEntry(activeNodeState)) return;
requestConfirmAction('saveIncremental');
};
const handleSaveOverwriteClick = () => {
if (runAdvancing || runMessaging || !canSaveWorldbookEntry(activeNodeState)) return;
requestConfirmAction('saveOverwrite');
};
const handleGraphNodeSelect = useCallback(
async (nodeId) => {
if (!runSessionEntered || !currentRun || nodeId === currentRun.currentNodeId) return;
const nodeState = currentRun.nodeStates?.find((n) => n.nodeId === nodeId);
if (!canSwitchToNode(nodeState)) return;
clearConfirmPending();
await switchRunNode(nodeId);
},
[runSessionEntered, currentRun, switchRunNode, clearConfirmPending]
);
const handleDeleteRun = async (runId) => {
const name = runDisplayName(runs.find((r) => r.id === runId) || {});
if (!window.confirm(`确定删除运行「${name}」?此操作不可撤销。`)) return;
@@ -772,7 +809,17 @@ function StudioRunPage() {
const inSession = runSessionEntered && !!currentRun;
const showToolCarousel =
inSession && runControls.includes('questions') && toolQuestions.length > 0;
const showNextStep = inSession && runControls.includes('nextStep');
const showNextStep =
inSession && shouldShowFirstExport(activeNodeState);
const showSaveIncremental =
inSession &&
hasRunControl(activeNodeState?.skillId, 'incrementalSave', skillTemplates) &&
shouldShowSaveAppendOverwrite(activeNodeState);
const showSaveOverwrite =
inSession &&
hasRunControl(activeNodeState?.skillId, 'overwriteSave', skillTemplates) &&
shouldShowSaveAppendOverwrite(activeNodeState);
const canSaveEntry = canSaveWorldbookEntry(activeNodeState);
const showUndo = runControls.includes('undo');
const showReroll = runControls.includes('reroll');
const canUndo = canUndoRun(activeNodeState);
@@ -840,6 +887,8 @@ function StudioRunPage() {
nodeStates={graphNodeStates}
currentNodeId={graphCurrentNodeId}
variant="sidebar"
onNodeSelect={runSessionEntered ? handleGraphNodeSelect : undefined}
canSelectNode={canSwitchToNode}
/>
);
};
@@ -1049,6 +1098,39 @@ function StudioRunPage() {
<p className="studio-run-stub-note">确认左侧目前产物无误后进入下一节点</p>
</div>
)}
{(showSaveIncremental || showSaveOverwrite) && (
<div className="studio-run-right-section studio-run-save-actions">
<h2 className="studio-run-section-title">世界书保存</h2>
<div className="studio-run-turn-actions__row">
{showSaveIncremental ? (
<ConfirmTurnButton
actionKey="saveIncremental"
pendingKey={confirmPending}
label="增量保存"
className="studio-run-turn-btn studio-run-turn-btn--append"
disabled={runAdvancing || runMessaging || !canSaveEntry}
title={canSaveEntry ? '追加为新世界书条目' : '请先生成当前步骤产物'}
onConfirm={handleSaveIncremental}
onRequestConfirm={handleSaveIncrementalClick}
/>
) : null}
{showSaveOverwrite ? (
<ConfirmTurnButton
actionKey="saveOverwrite"
pendingKey={confirmPending}
label="覆盖保存"
className="studio-run-turn-btn studio-run-turn-btn--overwrite"
disabled={runAdvancing || runMessaging || !canSaveEntry}
title={canSaveEntry ? '覆盖上次写入的条目' : '请先生成当前步骤产物'}
onConfirm={handleSaveOverwrite}
onRequestConfirm={handleSaveOverwriteClick}
/>
) : null}
</div>
<p className="studio-run-stub-note">增量保存会新建条目覆盖保存会更新本步骤上次写入的条目切换步骤请使用左侧节点图</p>
</div>
)}
</>
);