Studio: 增量/覆盖保存、节点切换与绑定编辑弹窗
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -376,14 +376,115 @@ class StudioRunService:
|
||||
except OSError as exc:
|
||||
raise ValueError(f"写入世界书失败:{exc}") from exc
|
||||
|
||||
def _overwrite_worldbook_entry(
|
||||
self,
|
||||
worldbook_name: str,
|
||||
node: StudioNode,
|
||||
draft: Dict[str, Any],
|
||||
entry_uid: str,
|
||||
) -> Dict[str, Any]:
|
||||
entry_payload = self._build_worldbook_entry_payload(node, draft)
|
||||
try:
|
||||
return worldbook_service.update_entry(
|
||||
worldbook_name, entry_uid, entry_payload
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
raise ValueError(f"世界书条目不存在或无法更新:{exc}") from exc
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"覆盖世界书失败:{exc}") from exc
|
||||
except OSError as exc:
|
||||
raise ValueError(f"覆盖世界书失败:{exc}") from exc
|
||||
|
||||
@staticmethod
|
||||
def _node_has_written_entry(state: StudioNodeRunState) -> bool:
|
||||
draft = state.lastDraft or {}
|
||||
return bool(draft.get("writtenEntryUid")) or state.status == "completed"
|
||||
|
||||
def _save_worldbook_entry_only(
|
||||
self,
|
||||
project_id: str,
|
||||
run_id: str,
|
||||
run: StudioRun,
|
||||
node: StudioNode,
|
||||
state: StudioNodeRunState,
|
||||
node_id: str,
|
||||
save_mode: str,
|
||||
) -> StudioRun:
|
||||
if not state.lastDraft:
|
||||
raise ValueError("请先生成并确认当前产物后再保存")
|
||||
|
||||
worldbook_name = self._resolve_bound_worldbook_name(project_id, run)
|
||||
draft = state.lastDraft
|
||||
|
||||
if save_mode == "append":
|
||||
entry_payload = self._build_worldbook_entry_payload(node, draft)
|
||||
if (draft.get("writtenEntryUid") or "").strip():
|
||||
try:
|
||||
wb_data = worldbook_service.get_worldbook(worldbook_name)
|
||||
entries = wb_data.get("entries") or []
|
||||
max_order = max(
|
||||
(int(e.get("order", 0)) for e in entries),
|
||||
default=0,
|
||||
)
|
||||
entry_payload["order"] = max_order + 1
|
||||
except (TypeError, ValueError):
|
||||
entry_payload["order"] = int(entry_payload.get("order", 100)) + 1
|
||||
written_entry = worldbook_service.append_entry(
|
||||
worldbook_name, entry_payload
|
||||
)
|
||||
else:
|
||||
written_entry = self._write_worldbook_entry_on_advance(
|
||||
worldbook_name, node, draft
|
||||
)
|
||||
elif save_mode == "overwrite":
|
||||
entry_uid = (draft.get("writtenEntryUid") or "").strip()
|
||||
if not entry_uid:
|
||||
raise ValueError(
|
||||
"尚未写入过世界书条目,请使用「下一步」完成首次导出,或使用「增量保存」"
|
||||
)
|
||||
written_entry = self._overwrite_worldbook_entry(
|
||||
worldbook_name, node, draft, entry_uid
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"不支持的保存模式:{save_mode}")
|
||||
|
||||
last_draft = copy.deepcopy(draft)
|
||||
last_draft["writtenEntryUid"] = written_entry.get("uid")
|
||||
last_draft["writtenWorldbookName"] = worldbook_name
|
||||
|
||||
now = datetime.now().isoformat()
|
||||
new_node_states: list[StudioNodeRunState] = []
|
||||
for ns in run.nodeStates:
|
||||
updated = ns.model_copy()
|
||||
if ns.nodeId == node_id:
|
||||
updated.lastDraft = last_draft
|
||||
new_node_states.append(updated)
|
||||
|
||||
updated_run = run.model_copy(
|
||||
update={
|
||||
"nodeStates": new_node_states,
|
||||
"updatedAt": now,
|
||||
}
|
||||
)
|
||||
self._save_run(project_id, run_id, updated_run)
|
||||
return updated_run
|
||||
|
||||
def advance_run(
|
||||
self,
|
||||
project_id: str,
|
||||
run_id: str,
|
||||
display_params: Optional[Dict[str, str]] = None,
|
||||
save_mode: str = "advance",
|
||||
) -> StudioRun:
|
||||
run = self.get_run(project_id, run_id)
|
||||
if run.status not in (StudioRunStatus.RUNNING, StudioRunStatus.PENDING):
|
||||
if save_mode in ("append", "overwrite"):
|
||||
if run.status not in (
|
||||
StudioRunStatus.RUNNING,
|
||||
StudioRunStatus.PENDING,
|
||||
StudioRunStatus.COMPLETED,
|
||||
):
|
||||
raise ValueError("运行已结束,无法保存")
|
||||
elif run.status not in (StudioRunStatus.RUNNING, StudioRunStatus.PENDING):
|
||||
raise ValueError("运行已结束,无法继续推进")
|
||||
|
||||
current_node_id = run.currentNodeId
|
||||
@@ -397,7 +498,25 @@ class StudioRunService:
|
||||
current_state = next(
|
||||
(s for s in run.nodeStates if s.nodeId == current_node_id), None
|
||||
)
|
||||
if not current_state or current_state.status != "active":
|
||||
if not current_state:
|
||||
raise ValueError("当前节点状态不存在")
|
||||
|
||||
if save_mode in ("append", "overwrite"):
|
||||
if current_node.skillId != "studio.worldbook_entry":
|
||||
raise ValueError("当前步骤不支持增量/覆盖保存")
|
||||
if current_state.status not in ("active", "completed"):
|
||||
raise ValueError("当前节点不可保存")
|
||||
return self._save_worldbook_entry_only(
|
||||
project_id,
|
||||
run_id,
|
||||
run,
|
||||
current_node,
|
||||
current_state,
|
||||
current_node_id,
|
||||
save_mode,
|
||||
)
|
||||
|
||||
if current_state.status != "active":
|
||||
raise ValueError("当前节点不可执行")
|
||||
|
||||
workflow_variables = dict(run.workflowVariables or {})
|
||||
@@ -456,6 +575,80 @@ class StudioRunService:
|
||||
self._save_run(project_id, run_id, updated_run)
|
||||
return updated_run
|
||||
|
||||
def save_run(
|
||||
self, project_id: str, run_id: str, mode: str
|
||||
) -> StudioRun:
|
||||
"""Save worldbook entry without advancing pipeline (incremental or overwrite)."""
|
||||
mode_map = {"incremental": "append", "overwrite": "overwrite"}
|
||||
if mode not in mode_map:
|
||||
raise ValueError(f"不支持的保存模式:{mode}")
|
||||
return self.advance_run(
|
||||
project_id, run_id, save_mode=mode_map[mode]
|
||||
)
|
||||
|
||||
def switch_run_node(
|
||||
self, project_id: str, run_id: str, node_id: str
|
||||
) -> StudioRun:
|
||||
"""Manually focus a pipeline node (e.g. revisit a completed worldbook step)."""
|
||||
run = self.get_run(project_id, run_id)
|
||||
if run.status not in (
|
||||
StudioRunStatus.RUNNING,
|
||||
StudioRunStatus.PENDING,
|
||||
StudioRunStatus.COMPLETED,
|
||||
):
|
||||
raise ValueError("运行已结束,无法切换节点")
|
||||
|
||||
target_node = _find_node(run.pipelineSnapshot, node_id)
|
||||
if not target_node:
|
||||
raise ValueError(f"节点不存在:{node_id}")
|
||||
if not target_node.enabled:
|
||||
raise ValueError("该节点已禁用,无法切换")
|
||||
if target_node.skillId != "studio.worldbook_entry":
|
||||
raise ValueError("仅支持切换到世界书创作步骤")
|
||||
|
||||
target_state = next(
|
||||
(s for s in run.nodeStates if s.nodeId == node_id), None
|
||||
)
|
||||
if not target_state:
|
||||
raise ValueError("节点状态不存在")
|
||||
if target_state.status not in ("active", "completed"):
|
||||
raise ValueError("仅可切换到进行中或已完成的步骤")
|
||||
|
||||
prev_node_id = run.currentNodeId
|
||||
now = datetime.now().isoformat()
|
||||
new_node_states: list[StudioNodeRunState] = []
|
||||
|
||||
for state in run.nodeStates:
|
||||
updated = state.model_copy()
|
||||
if state.nodeId == node_id:
|
||||
updated.status = "active"
|
||||
elif prev_node_id and state.nodeId == prev_node_id and prev_node_id != node_id:
|
||||
if state.skillId == "studio.worldbook_entry":
|
||||
if self._node_has_written_entry(state):
|
||||
updated.status = "completed"
|
||||
else:
|
||||
updated.status = "pending"
|
||||
new_node_states.append(updated)
|
||||
|
||||
new_status = (
|
||||
StudioRunStatus.COMPLETED
|
||||
if run.status == StudioRunStatus.COMPLETED
|
||||
and not _next_enabled_node_id(run.pipelineSnapshot, node_id)
|
||||
else StudioRunStatus.RUNNING
|
||||
)
|
||||
|
||||
updated_run = run.model_copy(
|
||||
update={
|
||||
"currentNodeId": node_id,
|
||||
"nodeStates": new_node_states,
|
||||
"status": new_status,
|
||||
"updatedAt": now,
|
||||
}
|
||||
)
|
||||
updated_run = store_context_on_run(updated_run, node_id)
|
||||
self._save_run(project_id, run_id, updated_run)
|
||||
return updated_run
|
||||
|
||||
async def send_run_message(
|
||||
self,
|
||||
project_id: str,
|
||||
|
||||
Reference in New Issue
Block a user