Studio: fix edit/run UI and write worldbook on advance (R6)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -30,6 +30,7 @@ from services.studio_step_respond import (
|
||||
studio_step_respond,
|
||||
studio_step_respond_stream,
|
||||
)
|
||||
from models.converters import WorldBookConverter
|
||||
from services.worldbook_service import worldbook_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -278,6 +279,103 @@ class StudioRunService:
|
||||
}
|
||||
return workflow_variables, last_draft
|
||||
|
||||
@staticmethod
|
||||
def _parse_keyword_field(raw: Any) -> List[str]:
|
||||
if raw is None:
|
||||
return []
|
||||
if isinstance(raw, list):
|
||||
return [str(x).strip() for x in raw if str(x).strip()]
|
||||
text = str(raw).strip()
|
||||
if not text:
|
||||
return []
|
||||
return [part.strip() for part in text.replace(",", ",").split(",") if part.strip()]
|
||||
|
||||
def _resolve_bound_worldbook_name(
|
||||
self, project_id: str, run: StudioRun
|
||||
) -> str:
|
||||
for state in run.nodeStates:
|
||||
if state.skillId != "studio.init_bind" or state.status != "completed":
|
||||
continue
|
||||
draft = state.lastDraft or {}
|
||||
name = (draft.get("worldbookName") or "").strip()
|
||||
if name:
|
||||
return name
|
||||
|
||||
try:
|
||||
project = studio_project_service.get_project(project_id)
|
||||
worldbook_id = project.meta.worldbookId
|
||||
except FileNotFoundError:
|
||||
worldbook_id = None
|
||||
|
||||
if worldbook_id:
|
||||
for summary in worldbook_service.list_worldbooks():
|
||||
wb_name = summary.get("name")
|
||||
if not wb_name:
|
||||
continue
|
||||
try:
|
||||
data = worldbook_service.get_worldbook(wb_name)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
if data.get("id") == worldbook_id:
|
||||
return wb_name
|
||||
|
||||
raise ValueError("未找到绑定的世界书,请先完成「创建并绑定」步骤")
|
||||
|
||||
def _build_worldbook_entry_payload(
|
||||
self, node: StudioNode, draft: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
insertion = (node.config or {}).get("insertion") or {}
|
||||
content = (draft.get("entryContent") or "").strip()
|
||||
if not content:
|
||||
raise ValueError("当前步骤产物为空,请先生成世界书条目内容后再进入下一步")
|
||||
|
||||
activation = (insertion.get("activationType") or "permanent").strip()
|
||||
position = insertion.get("position", 0)
|
||||
if isinstance(position, str):
|
||||
position = WorldBookConverter.POSITION_MAP_ST_TO_INTERNAL.get(position, 0)
|
||||
|
||||
key = self._parse_keyword_field(
|
||||
draft.get("insertionKey") or insertion.get("key")
|
||||
)
|
||||
keysecondary = self._parse_keyword_field(insertion.get("keysecondary"))
|
||||
comment = (
|
||||
(draft.get("insertionComment") or insertion.get("comment") or "").strip()
|
||||
or f"Studio · {node.displayName}"
|
||||
)
|
||||
|
||||
payload: Dict[str, Any] = {
|
||||
"content": content,
|
||||
"comment": comment,
|
||||
"activationType": activation,
|
||||
"position": position,
|
||||
"key": key,
|
||||
"keysecondary": keysecondary,
|
||||
"order": insertion.get("order", 100),
|
||||
"depth": insertion.get("depth", 4),
|
||||
"probability": insertion.get("probability", 100),
|
||||
"group": insertion.get("group") or [],
|
||||
"disable": bool(insertion.get("disable", False)),
|
||||
}
|
||||
if activation == "rag" and insertion.get("ragConfig"):
|
||||
payload["ragConfig"] = insertion["ragConfig"]
|
||||
return payload
|
||||
|
||||
def _write_worldbook_entry_on_advance(
|
||||
self,
|
||||
worldbook_name: str,
|
||||
node: StudioNode,
|
||||
draft: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
entry_payload = self._build_worldbook_entry_payload(node, draft)
|
||||
try:
|
||||
return worldbook_service.append_entry(worldbook_name, entry_payload)
|
||||
except FileNotFoundError:
|
||||
raise ValueError(f"世界书「{worldbook_name}」不存在,无法写入条目")
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"写入世界书失败:{exc}") from exc
|
||||
except OSError as exc:
|
||||
raise ValueError(f"写入世界书失败:{exc}") from exc
|
||||
|
||||
def advance_run(
|
||||
self,
|
||||
project_id: str,
|
||||
@@ -314,6 +412,15 @@ class StudioRunService:
|
||||
elif current_node.skillId == "studio.worldbook_entry":
|
||||
if not current_state.lastDraft:
|
||||
raise ValueError("请先生成并确认当前产物后再进入下一步")
|
||||
worldbook_name = self._resolve_bound_worldbook_name(project_id, run)
|
||||
written_entry = self._write_worldbook_entry_on_advance(
|
||||
worldbook_name,
|
||||
current_node,
|
||||
current_state.lastDraft,
|
||||
)
|
||||
last_draft = copy.deepcopy(current_state.lastDraft)
|
||||
last_draft["writtenEntryUid"] = written_entry.get("uid")
|
||||
last_draft["writtenWorldbookName"] = worldbook_name
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"技能「{current_node.skillId}」的执行尚未实现(R2+)"
|
||||
|
||||
@@ -232,6 +232,32 @@ class WorldBookService:
|
||||
|
||||
raise FileNotFoundError(f"Entry '{uid}' not found in worldbook '{name}'")
|
||||
|
||||
@staticmethod
|
||||
def append_entry(name: str, entry_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
在世界书中追加条目(规范化后写入,与 Chat 侧条目格式一致)。
|
||||
|
||||
Args:
|
||||
name: 世界书名称(文件名,不含 .json)
|
||||
entry_data: 条目字段(content、comment、activationType、position 等)
|
||||
|
||||
Returns:
|
||||
写入后的规范化条目
|
||||
"""
|
||||
data = WorldBookService._load_worldbook(name)
|
||||
if not data:
|
||||
raise FileNotFoundError(f"Worldbook '{name}' not found")
|
||||
|
||||
if not isinstance(data.get("entries"), list):
|
||||
data["entries"] = []
|
||||
|
||||
normalized = WorldBookConverter.normalize_entry(entry_data)
|
||||
data["entries"].append(normalized)
|
||||
now = int(datetime.now().timestamp())
|
||||
data["updatedAt"] = now
|
||||
WorldBookService._save_worldbook(name, data)
|
||||
return normalized
|
||||
|
||||
@staticmethod
|
||||
def create_entry(name: str, entry_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user