|
|
@@ -95,6 +95,150 @@ def _run_generate_job(job_id, manifest, creds):
|
|
|
_set_job(job_id, status="error", ok=False, error=str(e), updatedAt=time.time())
|
|
|
|
|
|
|
|
|
+def _load_library(game):
|
|
|
+ library_path = os.path.join(OUT_ROOT, game, "library.json")
|
|
|
+ if not os.path.isfile(library_path):
|
|
|
+ return {"game": game, "characters": [], "vfx": [], "ui": [], "ui_art": []}
|
|
|
+ with open(library_path, encoding="utf-8") as f:
|
|
|
+ return json.load(f)
|
|
|
+
|
|
|
+
|
|
|
+def _manifest_from_library(lib):
|
|
|
+ if lib.get("slot_config"):
|
|
|
+ return slot_workflow.complete_manifest({"slot_config": lib["slot_config"]})
|
|
|
+ return {
|
|
|
+ "game": lib.get("game") or "game",
|
|
|
+ "style": "",
|
|
|
+ "characters": [],
|
|
|
+ "ui_art": [],
|
|
|
+ "vfx": [],
|
|
|
+ "ui": [],
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+TASK_KIND_MAP = {
|
|
|
+ "characters": "characters",
|
|
|
+ "character": "characters",
|
|
|
+ "ui_art": "ui_art",
|
|
|
+ "art": "ui_art",
|
|
|
+ "vfx": "vfx",
|
|
|
+ "ui": "ui",
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+def _zh_name(kind, item, slot_config=None):
|
|
|
+ item_id = item.get("id", "")
|
|
|
+ role = item.get("role", "")
|
|
|
+ known = {
|
|
|
+ "wild": "百搭符号",
|
|
|
+ "scatter": "免费旋转触发符号",
|
|
|
+ "coin_cash": "现金金币符号",
|
|
|
+ "collect": "收集符号",
|
|
|
+ "bg_main": "主背景",
|
|
|
+ "cover": "封面图",
|
|
|
+ "logo": "游戏 Logo",
|
|
|
+ "reel_frame": "卷轴框",
|
|
|
+ "btn_spin": "旋转按钮",
|
|
|
+ "btn_round": "通用圆按钮",
|
|
|
+ "hud_pill": "HUD 信息条",
|
|
|
+ "boss_explosion": "大魔王裂开爆炸特效",
|
|
|
+ }
|
|
|
+ if role == "boss" or item_id == (slot_config or {}).get("boss", {}).get("id"):
|
|
|
+ return (slot_config or {}).get("boss", {}).get("title") or "大魔王关主"
|
|
|
+ if item_id in known:
|
|
|
+ return known[item_id]
|
|
|
+ if item_id.startswith("jelly_"):
|
|
|
+ return "果冻角色 " + item_id.replace("jelly_", "")
|
|
|
+ if kind == "vfx":
|
|
|
+ return "粒子特效 " + item_id
|
|
|
+ if kind == "ui":
|
|
|
+ return "界面动效 " + item_id
|
|
|
+ return "素材 " + item_id
|
|
|
+
|
|
|
+
|
|
|
+def _task_use(kind, item):
|
|
|
+ item_id = item.get("id", "")
|
|
|
+ role = item.get("role", "")
|
|
|
+ if role == "boss":
|
|
|
+ return "关主角色。待机时 watch/charge,玩家赢时撒币并裂开爆炸,玩家输时举剑踩踏耀武扬威。"
|
|
|
+ if kind == "characters":
|
|
|
+ return "卷轴符号或可动画角色,用于 slot 盘面和中奖反馈。"
|
|
|
+ if kind == "ui_art":
|
|
|
+ if item_id == "bg_main":
|
|
|
+ return "游戏主场景背景。"
|
|
|
+ if item_id == "cover":
|
|
|
+ return "封面、分享图或入口展示。"
|
|
|
+ return "界面美术元素,用于 Cocos 原型的背景、按钮、框体或 HUD。"
|
|
|
+ if kind == "vfx":
|
|
|
+ return "粒子特效配置,用于中奖、金币雨、爆炸或强调反馈。"
|
|
|
+ return "UI tween 动效预设,用于按钮、弹窗、数字滚动等界面反馈。"
|
|
|
+
|
|
|
+
|
|
|
+def _task_prompt(kind, item):
|
|
|
+ if kind == "characters" and item.get("type") == "spine_parts":
|
|
|
+ parts = item.get("parts") or []
|
|
|
+ part_text = "\n".join([f"- {p.get('id')}: {p.get('prompt', '')}" for p in parts])
|
|
|
+ return (item.get("prompt", "") + ("\n拆件:\n" + part_text if part_text else "")).strip()
|
|
|
+ return item.get("prompt") or item.get("template") or item.get("preset") or ""
|
|
|
+
|
|
|
+
|
|
|
+def _build_tasks(lib):
|
|
|
+ manifest = _manifest_from_library(lib)
|
|
|
+ slot_config = manifest.get("slot_config", {})
|
|
|
+ existing = {
|
|
|
+ "characters": {x.get("id"): x for x in lib.get("characters", [])},
|
|
|
+ "ui_art": {x.get("id"): x for x in lib.get("ui_art", [])},
|
|
|
+ "vfx": {x.get("id"): x for x in lib.get("vfx", [])},
|
|
|
+ "ui": {x.get("id"): x for x in lib.get("ui", [])},
|
|
|
+ }
|
|
|
+ tasks = {k: [] for k in ("characters", "ui_art", "vfx", "ui")}
|
|
|
+ for kind in tasks:
|
|
|
+ for item in manifest.get(kind, []):
|
|
|
+ item_id = item.get("id")
|
|
|
+ asset = existing[kind].get(item_id)
|
|
|
+ tasks[kind].append({
|
|
|
+ "kind": kind,
|
|
|
+ "id": item_id,
|
|
|
+ "englishName": item_id,
|
|
|
+ "chineseName": _zh_name(kind, item, slot_config),
|
|
|
+ "use": _task_use(kind, item),
|
|
|
+ "prompt": _task_prompt(kind, item),
|
|
|
+ "status": "done" if asset else "missing",
|
|
|
+ "asset": asset,
|
|
|
+ "assetType": item.get("type") or kind,
|
|
|
+ "animations": item.get("animations", []),
|
|
|
+ "transparent": item.get("transparent"),
|
|
|
+ "size": item.get("size"),
|
|
|
+ })
|
|
|
+ return tasks
|
|
|
+
|
|
|
+
|
|
|
+def _filter_manifest_tasks(manifest, kind, task_ids):
|
|
|
+ wanted = set(task_ids)
|
|
|
+ filtered = copy_manifest = json.loads(json.dumps(manifest, ensure_ascii=False))
|
|
|
+ for key in ("characters", "ui_art", "vfx", "ui"):
|
|
|
+ filtered[key] = [x for x in manifest.get(key, []) if key == kind and x.get("id") in wanted]
|
|
|
+ return filtered
|
|
|
+
|
|
|
+
|
|
|
+def _run_retry_job(job_id, game, kind, task_ids, creds):
|
|
|
+ _set_job(job_id, status="running", logs=[], game=game, startedAt=time.time(), updatedAt=time.time())
|
|
|
+ try:
|
|
|
+ lib = _load_library(game)
|
|
|
+ manifest = _manifest_from_library(lib)
|
|
|
+ partial = _filter_manifest_tasks(manifest, kind, task_ids)
|
|
|
+ labels = ", ".join(task_ids)
|
|
|
+ _append_job_log(job_id, f"补生成任务:{kind} / {labels}")
|
|
|
+ new_lib, _ = pipeline.run(partial, OUT_ROOT, creds=creds,
|
|
|
+ log=lambda m: _append_job_log(job_id, m),
|
|
|
+ merge_existing=True)
|
|
|
+ _set_job(job_id, status="done", ok=True, game=new_lib["game"], updatedAt=time.time())
|
|
|
+ except Exception as e:
|
|
|
+ traceback.print_exc()
|
|
|
+ _append_job_log(job_id, f"❌ 补生成失败: {e}")
|
|
|
+ _set_job(job_id, status="error", ok=False, error=str(e), updatedAt=time.time())
|
|
|
+
|
|
|
+
|
|
|
def _split_lines(value):
|
|
|
if isinstance(value, list):
|
|
|
return [str(x).strip() for x in value if str(x).strip()]
|
|
|
@@ -367,15 +511,22 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
game = (qs.get("game", [None])[0]) or (games[-1] if games else None)
|
|
|
if not game:
|
|
|
return self._send(200, {"game": None, "games": games,
|
|
|
- "characters": [], "vfx": [], "ui": []})
|
|
|
+ "characters": [], "vfx": [], "ui": [], "tasks": {}})
|
|
|
library_path = os.path.join(OUT_ROOT, game, "library.json")
|
|
|
if not os.path.isfile(library_path):
|
|
|
return self._send(200, {"game": game, "games": games, "assetBase": f"/assets/{game}/",
|
|
|
- "characters": [], "vfx": [], "ui": []})
|
|
|
+ "characters": [], "vfx": [], "ui": [], "tasks": {}})
|
|
|
with open(library_path, encoding="utf-8") as f:
|
|
|
lib = json.load(f)
|
|
|
lib["games"] = games
|
|
|
lib["assetBase"] = f"/assets/{game}/"
|
|
|
+ lib["tasks"] = _build_tasks(lib)
|
|
|
+ all_tasks = [t for rows in lib["tasks"].values() for t in rows]
|
|
|
+ lib["taskSummary"] = {
|
|
|
+ "total": len(all_tasks),
|
|
|
+ "done": sum(1 for t in all_tasks if t.get("status") == "done"),
|
|
|
+ "missing": sum(1 for t in all_tasks if t.get("status") != "done"),
|
|
|
+ }
|
|
|
return self._send(200, lib)
|
|
|
if path.startswith("/assets/"):
|
|
|
rel = unquote(path[len("/assets/"):])
|
|
|
@@ -402,6 +553,8 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
return self._post_export()
|
|
|
if route == "/api/open-folder":
|
|
|
return self._post_open_folder()
|
|
|
+ if route == "/api/retry-task":
|
|
|
+ return self._post_retry_task()
|
|
|
if route == "/api/delete":
|
|
|
return self._post_delete()
|
|
|
if route == "/api/slot-workflow":
|
|
|
@@ -508,6 +661,44 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
return self._send(500, {"ok": False, "error": f"打开素材目录失败: {e}"})
|
|
|
return self._send(200, {"ok": True, "game": game, "path": target})
|
|
|
|
|
|
+ def _post_retry_task(self):
|
|
|
+ try:
|
|
|
+ data = self._read_json_body()
|
|
|
+ except Exception as e:
|
|
|
+ return self._send(400, {"ok": False, "error": f"请求体不是合法 JSON: {e}"})
|
|
|
+ game = (data.get("game") or "").strip()
|
|
|
+ if not game or game not in list_games():
|
|
|
+ return self._send(400, {"ok": False, "error": f"无效的 game: {game!r}"})
|
|
|
+ kind = TASK_KIND_MAP.get((data.get("kind") or "").strip())
|
|
|
+ if not kind:
|
|
|
+ return self._send(400, {"ok": False, "error": "无效的任务类型"})
|
|
|
+ task_ids = data.get("ids") or data.get("taskIds") or [data.get("id")]
|
|
|
+ if isinstance(task_ids, str):
|
|
|
+ task_ids = [task_ids]
|
|
|
+ task_ids = [str(x).strip() for x in task_ids if str(x or "").strip()]
|
|
|
+ if not task_ids:
|
|
|
+ return self._send(400, {"ok": False, "error": "没有选择要重试的任务"})
|
|
|
+
|
|
|
+ lib = _load_library(game)
|
|
|
+ tasks = _build_tasks(lib).get(kind, [])
|
|
|
+ valid_ids = {t["id"] for t in tasks}
|
|
|
+ bad = [x for x in task_ids if x not in valid_ids]
|
|
|
+ if bad:
|
|
|
+ return self._send(400, {"ok": False, "error": f"任务不存在: {', '.join(bad)}"})
|
|
|
+ creds = {
|
|
|
+ "provider": data.get("provider", "OpenAI 兼容接口"),
|
|
|
+ "api_key": (data.get("api_key") or DEFAULT_API_KEY).strip(),
|
|
|
+ "base_url": (data.get("base_url") or DEFAULT_BASE_URL).strip(),
|
|
|
+ "model": (data.get("model") or DEFAULT_IMAGE_MODEL).strip(),
|
|
|
+ "size": data.get("size", "1024x1024"),
|
|
|
+ }
|
|
|
+ job_id = uuid.uuid4().hex
|
|
|
+ with JOBS_LOCK:
|
|
|
+ JOBS[job_id] = {"status": "queued", "ok": None, "logs": ["补生成任务已创建,等待开始…"],
|
|
|
+ "game": game, "createdAt": time.time(), "updatedAt": time.time()}
|
|
|
+ threading.Thread(target=_run_retry_job, args=(job_id, game, kind, task_ids, creds), daemon=True).start()
|
|
|
+ return self._send(200, {"ok": True, "jobId": job_id, "game": game})
|
|
|
+
|
|
|
def _post_delete(self):
|
|
|
try:
|
|
|
data = self._read_json_body()
|