feat: pipeline Sage前端桥接模块
- 通过HTTP调用Hermes Pipeline API - 产线列表/详情/提交/节点查看 - CSS+JS前端样式
This commit is contained in:
parent
5cc987efc0
commit
e75e6f77e1
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.egg-info/
|
||||||
|
build/
|
||||||
6
pipeline/__init__.py
Normal file
6
pipeline/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
"""Pipeline module - Sage frontend for Hermes pipeline backend"""
|
||||||
|
|
||||||
|
from .init import (
|
||||||
|
load_pipeline,
|
||||||
|
get_pipeline_config,
|
||||||
|
)
|
||||||
87
pipeline/init.py
Normal file
87
pipeline/init.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
"""Pipeline module initialization - bridges Sage frontend to Hermes pipeline API"""
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import json
|
||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
|
||||||
|
MODULE_NAME = "pipeline"
|
||||||
|
MODULE_VERSION = "1.0.0"
|
||||||
|
|
||||||
|
# Hermes Pipeline API base URL (configurable)
|
||||||
|
PIPELINE_API_BASE = "https://token.opencomputing.cn/pipeline/v1"
|
||||||
|
|
||||||
|
|
||||||
|
async def _hermes_request(method, path, data=None, timeout=120):
|
||||||
|
"""Generic HTTP request to Hermes pipeline API"""
|
||||||
|
url = f"{PIPELINE_API_BASE}{path}"
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
kwargs = {"timeout": aiohttp.ClientTimeout(total=timeout)}
|
||||||
|
if method == "GET":
|
||||||
|
async with session.get(url, **kwargs) as resp:
|
||||||
|
return await resp.json()
|
||||||
|
elif method == "POST":
|
||||||
|
async with session.post(url, json=data, **kwargs) as resp:
|
||||||
|
return await resp.json()
|
||||||
|
elif method == "PUT":
|
||||||
|
async with session.put(url, json=data, **kwargs) as resp:
|
||||||
|
return await resp.json()
|
||||||
|
return {"status": "error", "message": f"Unknown method: {method}"}
|
||||||
|
|
||||||
|
|
||||||
|
async def hermes_pipeline_list(user_id):
|
||||||
|
"""Get all pipelines for a user"""
|
||||||
|
return await _hermes_request("GET", f"/tasks?user_id={user_id}")
|
||||||
|
|
||||||
|
|
||||||
|
async def hermes_pipeline_detail(pipeline_id):
|
||||||
|
"""Get pipeline detail with node tree"""
|
||||||
|
return await _hermes_request("GET", f"/task/{pipeline_id}")
|
||||||
|
|
||||||
|
|
||||||
|
async def hermes_pipeline_submit(data):
|
||||||
|
"""Submit a new pipeline task"""
|
||||||
|
return await _hermes_request("POST", "/task/submit", data, timeout=30)
|
||||||
|
|
||||||
|
|
||||||
|
async def hermes_pipeline_node(pipeline_id, step, version=None):
|
||||||
|
"""Get a specific node's input and output"""
|
||||||
|
path = f"/task/{pipeline_id}/node/{step}"
|
||||||
|
if version:
|
||||||
|
path += f"?version={version}"
|
||||||
|
return await _hermes_request("GET", path)
|
||||||
|
|
||||||
|
|
||||||
|
async def hermes_pipeline_modify(pipeline_id, updates, rerun_from="node"):
|
||||||
|
"""Modify node artifacts and rerun pipeline.
|
||||||
|
|
||||||
|
rerun_from:
|
||||||
|
- 'node': rerun from the modified node itself (input changed)
|
||||||
|
- 'next': rerun from the node after the modified one (output changed)
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
"pipeline_id": pipeline_id,
|
||||||
|
"updates": updates,
|
||||||
|
"rerun_from": rerun_from,
|
||||||
|
}
|
||||||
|
return await _hermes_request("POST", "/task/update", data, timeout=60)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pipeline_config():
|
||||||
|
"""Return pipeline module configuration"""
|
||||||
|
return {
|
||||||
|
"api_base": PIPELINE_API_BASE,
|
||||||
|
"module_name": MODULE_NAME,
|
||||||
|
"version": MODULE_VERSION,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_pipeline():
|
||||||
|
"""Register all pipeline functions with ServerEnv"""
|
||||||
|
env = ServerEnv()
|
||||||
|
env.hermes_pipeline_list = hermes_pipeline_list
|
||||||
|
env.hermes_pipeline_detail = hermes_pipeline_detail
|
||||||
|
env.hermes_pipeline_submit = hermes_pipeline_submit
|
||||||
|
env.hermes_pipeline_node = hermes_pipeline_node
|
||||||
|
env.hermes_pipeline_modify = hermes_pipeline_modify
|
||||||
|
env.get_pipeline_config = get_pipeline_config
|
||||||
|
return True
|
||||||
13
wwwroot/api/pipeline_detail.dspy
Normal file
13
wwwroot/api/pipeline_detail.dspy
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
user_id = await get_user()
|
||||||
|
if not user_id:
|
||||||
|
return json.dumps({"status": "error", "message": "未登录"}, ensure_ascii=False)
|
||||||
|
|
||||||
|
pipeline_id = params_kw.get('pipeline_id', '')
|
||||||
|
if not pipeline_id:
|
||||||
|
return json.dumps({"status": "error", "message": "缺少pipeline_id"}, ensure_ascii=False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await hermes_pipeline_detail(pipeline_id)
|
||||||
|
return json.dumps(result, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)
|
||||||
9
wwwroot/api/pipeline_list.dspy
Normal file
9
wwwroot/api/pipeline_list.dspy
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
user_id = await get_user()
|
||||||
|
if not user_id:
|
||||||
|
return json.dumps({"status": "error", "message": "未登录"}, ensure_ascii=False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await hermes_pipeline_list(user_id)
|
||||||
|
return json.dumps(result, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)
|
||||||
25
wwwroot/api/pipeline_modify.dspy
Normal file
25
wwwroot/api/pipeline_modify.dspy
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
user_id = await get_user()
|
||||||
|
if not user_id:
|
||||||
|
return json.dumps({"status": "error", "message": "未登录"}, ensure_ascii=False)
|
||||||
|
|
||||||
|
pipeline_id = params_kw.get('pipeline_id', '')
|
||||||
|
step = params_kw.get('step', '')
|
||||||
|
modify_type = params_kw.get('modify_type', 'input') # 'input' or 'output'
|
||||||
|
content = params_kw.get('content', '')
|
||||||
|
|
||||||
|
if not pipeline_id or not step or not content:
|
||||||
|
return json.dumps({"status": "error", "message": "缺少必要参数"}, ensure_ascii=False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
content_data = json.loads(content)
|
||||||
|
except Exception:
|
||||||
|
content_data = content
|
||||||
|
|
||||||
|
updates = {step: {"content": content_data}}
|
||||||
|
rerun_from = "node" if modify_type == "input" else "next"
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await hermes_pipeline_modify(pipeline_id, updates, rerun_from)
|
||||||
|
return json.dumps(result, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)
|
||||||
16
wwwroot/api/pipeline_node.dspy
Normal file
16
wwwroot/api/pipeline_node.dspy
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
user_id = await get_user()
|
||||||
|
if not user_id:
|
||||||
|
return json.dumps({"status": "error", "message": "未登录"}, ensure_ascii=False)
|
||||||
|
|
||||||
|
pipeline_id = params_kw.get('pipeline_id', '')
|
||||||
|
step = params_kw.get('step', '')
|
||||||
|
version = params_kw.get('version', None)
|
||||||
|
|
||||||
|
if not pipeline_id or not step:
|
||||||
|
return json.dumps({"status": "error", "message": "缺少pipeline_id或step"}, ensure_ascii=False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await hermes_pipeline_node(pipeline_id, step, version)
|
||||||
|
return json.dumps(result, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)
|
||||||
29
wwwroot/api/pipeline_submit.dspy
Normal file
29
wwwroot/api/pipeline_submit.dspy
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
user_id = await get_user()
|
||||||
|
if not user_id:
|
||||||
|
return json.dumps({"status": "error", "message": "未登录"}, ensure_ascii=False)
|
||||||
|
|
||||||
|
mode = params_kw.get('mode', '')
|
||||||
|
title = params_kw.get('title', '')
|
||||||
|
lyrics = params_kw.get('lyrics', '')
|
||||||
|
|
||||||
|
if not mode:
|
||||||
|
return json.dumps({"status": "error", "message": "缺少mode参数"}, ensure_ascii=False)
|
||||||
|
|
||||||
|
submit_data = {
|
||||||
|
"mode": mode,
|
||||||
|
"title": title,
|
||||||
|
"lyrics": lyrics,
|
||||||
|
"user_id": user_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 收集其他可选参数
|
||||||
|
for key in ['input_audio', 'input_video', 'outline', 'lyricist', 'composer', 'scene']:
|
||||||
|
val = params_kw.get(key, '')
|
||||||
|
if val:
|
||||||
|
submit_data[key] = val
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await hermes_pipeline_submit(submit_data)
|
||||||
|
return json.dumps(result, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)
|
||||||
122
wwwroot/pipeline.css
Normal file
122
wwwroot/pipeline.css
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
/* Pipeline Module Styles */
|
||||||
|
|
||||||
|
.pipeline-container { padding: 20px; height: 100%; overflow-y: auto; }
|
||||||
|
|
||||||
|
.pipeline-header {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
margin-bottom: 20px; padding-bottom: 12px; border-bottom: 2px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
.pipeline-header h2 { margin: 0; font-size: 22px; color: #333; }
|
||||||
|
|
||||||
|
.pipeline-btn {
|
||||||
|
padding: 8px 18px; border: none; border-radius: 6px; cursor: pointer;
|
||||||
|
font-size: 14px; font-weight: 500; transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.pipeline-btn-primary { background: #4a90d9; color: #fff; }
|
||||||
|
.pipeline-btn-primary:hover { background: #357abd; }
|
||||||
|
.pipeline-btn-secondary { background: #e8e8e8; color: #333; }
|
||||||
|
.pipeline-btn-secondary:hover { background: #d5d5d5; }
|
||||||
|
.pipeline-btn-danger { background: #e74c3c; color: #fff; }
|
||||||
|
.pipeline-btn-danger:hover { background: #c0392b; }
|
||||||
|
.pipeline-btn-sm { padding: 4px 12px; font-size: 12px; }
|
||||||
|
|
||||||
|
.pipeline-task-list { list-style: none; padding: 0; margin: 0; }
|
||||||
|
.pipeline-task-item {
|
||||||
|
display: flex; align-items: center; padding: 14px 16px;
|
||||||
|
border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 10px;
|
||||||
|
cursor: pointer; transition: all 0.2s; background: #fff;
|
||||||
|
}
|
||||||
|
.pipeline-task-item:hover { border-color: #4a90d9; box-shadow: 0 2px 8px rgba(74,144,217,0.12); }
|
||||||
|
.pipeline-task-id { font-family: monospace; font-size: 13px; color: #4a90d9; font-weight: 600; min-width: 180px; }
|
||||||
|
.pipeline-task-title { flex: 1; font-size: 15px; color: #333; margin: 0 16px; }
|
||||||
|
.pipeline-task-meta { font-size: 12px; color: #888; display: flex; gap: 16px; align-items: center; }
|
||||||
|
|
||||||
|
.pipeline-status {
|
||||||
|
padding: 3px 10px; border-radius: 12px; font-size: 11px; font-weight: 600;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.status-completed { background: #d4edda; color: #155724; }
|
||||||
|
.status-running { background: #fff3cd; color: #856404; }
|
||||||
|
.status-failed { background: #f8d7da; color: #721c24; }
|
||||||
|
.status-submitted { background: #d1ecf1; color: #0c5460; }
|
||||||
|
|
||||||
|
/* Task Detail */
|
||||||
|
.pipeline-detail-header {
|
||||||
|
display: flex; align-items: center; gap: 16px; margin-bottom: 20px;
|
||||||
|
padding-bottom: 12px; border-bottom: 2px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
.pipeline-back-btn {
|
||||||
|
background: none; border: 1px solid #ccc; border-radius: 6px;
|
||||||
|
padding: 6px 14px; cursor: pointer; font-size: 13px; color: #666;
|
||||||
|
}
|
||||||
|
.pipeline-back-btn:hover { background: #f0f0f0; }
|
||||||
|
.pipeline-detail-title { font-size: 20px; font-weight: 600; color: #333; }
|
||||||
|
.pipeline-detail-meta { font-size: 13px; color: #888; margin-top: 4px; }
|
||||||
|
|
||||||
|
/* Node Tree */
|
||||||
|
.pipeline-node-tree { padding: 0; }
|
||||||
|
.pipeline-node-card {
|
||||||
|
border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 8px;
|
||||||
|
overflow: hidden; background: #fff; position: relative;
|
||||||
|
}
|
||||||
|
.pipeline-node-card::before {
|
||||||
|
content: ''; position: absolute; left: 20px; top: -8px;
|
||||||
|
width: 2px; height: 8px; background: #d0d0d0;
|
||||||
|
}
|
||||||
|
.pipeline-node-card:first-child::before { display: none; }
|
||||||
|
.pipeline-node-header {
|
||||||
|
display: flex; align-items: center; padding: 12px 16px;
|
||||||
|
cursor: pointer; transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.pipeline-node-header:hover { background: #f8f9fa; }
|
||||||
|
.pipeline-node-icon { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; margin-right: 12px; flex-shrink: 0; }
|
||||||
|
.node-icon-completed { background: #d4edda; color: #155724; }
|
||||||
|
.node-icon-running { background: #fff3cd; color: #856404; }
|
||||||
|
.node-icon-pending { background: #e8e8e8; color: #888; }
|
||||||
|
.node-icon-failed { background: #f8d7da; color: #721c24; }
|
||||||
|
.pipeline-node-name { flex: 1; font-size: 14px; font-weight: 500; color: #333; }
|
||||||
|
.pipeline-node-version { font-size: 11px; color: #888; background: #f0f0f0; padding: 2px 8px; border-radius: 10px; margin-left: 8px; }
|
||||||
|
|
||||||
|
.pipeline-node-body { padding: 0 16px 16px; display: none; border-top: 1px solid #f0f0f0; }
|
||||||
|
.pipeline-node-body.expanded { display: block; padding-top: 12px; }
|
||||||
|
|
||||||
|
.pipeline-io-section { margin-bottom: 12px; }
|
||||||
|
.pipeline-io-label { font-size: 12px; font-weight: 600; color: #666; margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||||
|
.pipeline-io-content {
|
||||||
|
background: #f8f9fa; border: 1px solid #e8e8e8; border-radius: 6px;
|
||||||
|
padding: 10px; font-family: monospace; font-size: 12px;
|
||||||
|
max-height: 200px; overflow-y: auto; white-space: pre-wrap; word-break: break-all;
|
||||||
|
}
|
||||||
|
.pipeline-io-actions { display: flex; gap: 8px; margin-top: 8px; }
|
||||||
|
|
||||||
|
/* Create Form */
|
||||||
|
.pipeline-form { max-width: 600px; }
|
||||||
|
.pipeline-form-group { margin-bottom: 18px; }
|
||||||
|
.pipeline-form-label { display: block; font-size: 13px; font-weight: 600; color: #555; margin-bottom: 6px; }
|
||||||
|
.pipeline-form-input, .pipeline-form-select, .pipeline-form-textarea {
|
||||||
|
width: 100%; padding: 10px 12px; border: 1px solid #d0d0d0;
|
||||||
|
border-radius: 6px; font-size: 14px; box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.pipeline-form-input:focus, .pipeline-form-select:focus, .pipeline-form-textarea:focus {
|
||||||
|
border-color: #4a90d9; outline: none; box-shadow: 0 0 0 3px rgba(74,144,217,0.1);
|
||||||
|
}
|
||||||
|
.pipeline-form-textarea { min-height: 150px; resize: vertical; font-family: inherit; }
|
||||||
|
|
||||||
|
/* Edit Modal */
|
||||||
|
.pipeline-edit-section {
|
||||||
|
background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 8px;
|
||||||
|
padding: 16px; margin-top: 12px;
|
||||||
|
}
|
||||||
|
.pipeline-edit-textarea {
|
||||||
|
width: 100%; min-height: 200px; padding: 10px; border: 1px solid #d0d0d0;
|
||||||
|
border-radius: 6px; font-family: monospace; font-size: 12px;
|
||||||
|
resize: vertical; box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.pipeline-edit-type { display: flex; gap: 12px; margin-bottom: 12px; }
|
||||||
|
.pipeline-edit-type label { display: flex; align-items: center; gap: 4px; font-size: 13px; cursor: pointer; }
|
||||||
|
.pipeline-edit-actions { display: flex; gap: 8px; margin-top: 12px; }
|
||||||
|
|
||||||
|
.pipeline-empty { text-align: center; padding: 60px 20px; color: #aaa; }
|
||||||
|
.pipeline-loading { text-align: center; padding: 40px; color: #888; font-size: 14px; }
|
||||||
|
.pipeline-error { background: #fff3f3; border: 1px solid #fcc; border-radius: 6px; padding: 12px; color: #c00; font-size: 13px; margin: 10px 0; }
|
||||||
492
wwwroot/pipeline.js
Normal file
492
wwwroot/pipeline.js
Normal file
@ -0,0 +1,492 @@
|
|||||||
|
/* Pipeline Module - Main JavaScript */
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var API_BASE = ''; // Will be set on init
|
||||||
|
var currentView = 'list';
|
||||||
|
var currentPipelineId = null;
|
||||||
|
|
||||||
|
// === Initialization ===
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
// Detect API base from current URL
|
||||||
|
var base = window.location.pathname.replace(/\/pipeline\/index\.ui.*$/, '/pipeline');
|
||||||
|
API_BASE = base;
|
||||||
|
showListView();
|
||||||
|
}
|
||||||
|
|
||||||
|
// === API Helpers ===
|
||||||
|
|
||||||
|
function apiGet(path, callback) {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', API_BASE + path, true);
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState === 4) {
|
||||||
|
try {
|
||||||
|
var data = JSON.parse(xhr.responseText);
|
||||||
|
callback(null, data);
|
||||||
|
} catch(e) {
|
||||||
|
callback('解析响应失败', null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = function() { callback('网络请求失败', null); };
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
function apiPost(path, body, callback) {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', API_BASE + path, true);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState === 4) {
|
||||||
|
try {
|
||||||
|
var data = JSON.parse(xhr.responseText);
|
||||||
|
callback(null, data);
|
||||||
|
} catch(e) {
|
||||||
|
callback('解析响应失败', null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = function() { callback('网络请求失败', null); };
|
||||||
|
xhr.send(JSON.stringify(body));
|
||||||
|
}
|
||||||
|
|
||||||
|
// === View Rendering ===
|
||||||
|
|
||||||
|
function getContainer() {
|
||||||
|
return document.getElementById('pipeline_content');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showListView() {
|
||||||
|
currentView = 'list';
|
||||||
|
currentPipelineId = null;
|
||||||
|
var c = getContainer();
|
||||||
|
if (!c) return;
|
||||||
|
c.innerHTML = '<div class="pipeline-container">' +
|
||||||
|
'<div class="pipeline-header">' +
|
||||||
|
'<h2>产线任务</h2>' +
|
||||||
|
'<button class="pipeline-btn pipeline-btn-primary" onclick="PipelineUI.showCreateView()">+ 新建任务</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div id="pipeline_list_body"><div class="pipeline-loading">加载中...</div></div>' +
|
||||||
|
'</div>';
|
||||||
|
loadTaskList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTaskList() {
|
||||||
|
apiGet('/api/pipeline_list.dspy', function(err, data) {
|
||||||
|
var el = document.getElementById('pipeline_list_body');
|
||||||
|
if (!el) return;
|
||||||
|
if (err) { el.innerHTML = '<div class="pipeline-error">' + err + '</div>'; return; }
|
||||||
|
if (data.status === 'error') { el.innerHTML = '<div class="pipeline-error">' + (data.message || '加载失败') + '</div>'; return; }
|
||||||
|
|
||||||
|
var tasks = data.tasks || data.data || [];
|
||||||
|
if (!tasks.length) {
|
||||||
|
el.innerHTML = '<div class="pipeline-empty">暂无产线任务<br><br><button class="pipeline-btn pipeline-btn-primary" onclick="PipelineUI.showCreateView()">创建第一个任务</button></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var html = '<ul class="pipeline-task-list">';
|
||||||
|
for (var i = 0; i < tasks.length; i++) {
|
||||||
|
var t = tasks[i];
|
||||||
|
var st = t.state || 'unknown';
|
||||||
|
var stClass = 'status-' + st;
|
||||||
|
var stText = {completed:'已完成', running:'运行中', failed:'失败', submitted:'已提交', processing:'处理中'}[st] || st;
|
||||||
|
html += '<li class="pipeline-task-item" onclick="PipelineUI.showDetailView(\'' + t.pipeline_id + '\')">' +
|
||||||
|
'<span class="pipeline-task-id">' + esc(t.pipeline_id) + '</span>' +
|
||||||
|
'<span class="pipeline-task-title">' + esc(t.title || t.mode || '-') + '</span>' +
|
||||||
|
'<span class="pipeline-task-meta">' +
|
||||||
|
'<span>' + esc(t.mode || '') + '</span>' +
|
||||||
|
'<span class="pipeline-status ' + stClass + '">' + stText + '</span>' +
|
||||||
|
'<span>v' + (t.current_version || 1) + '</span>' +
|
||||||
|
'</span></li>';
|
||||||
|
}
|
||||||
|
html += '</ul>';
|
||||||
|
el.innerHTML = html;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCreateView() {
|
||||||
|
currentView = 'create';
|
||||||
|
var c = getContainer();
|
||||||
|
if (!c) return;
|
||||||
|
c.innerHTML = '<div class="pipeline-container">' +
|
||||||
|
'<div class="pipeline-detail-header">' +
|
||||||
|
'<button class="pipeline-back-btn" onclick="PipelineUI.showListView()">← 返回</button>' +
|
||||||
|
'<span class="pipeline-detail-title">新建产线任务</span></div>' +
|
||||||
|
'<div class="pipeline-form" id="pipeline_create_form">' +
|
||||||
|
|
||||||
|
'<div class="pipeline-form-group">' +
|
||||||
|
'<label class="pipeline-form-label">产线模式</label>' +
|
||||||
|
'<select class="pipeline-form-select" id="pf_mode" onchange="PipelineUI.onModeChange()">' +
|
||||||
|
'<option value="audio_lyrics">音频+歌词 (Mode A)</option>' +
|
||||||
|
'<option value="video_lyrics">视频+歌词 (Mode B)</option>' +
|
||||||
|
'<option value="lyrics_only" selected>仅歌词/大纲 (Mode C)</option>' +
|
||||||
|
'</select></div>' +
|
||||||
|
|
||||||
|
'<div class="pipeline-form-group" id="pf_audio_group" style="display:none">' +
|
||||||
|
'<label class="pipeline-form-label">音频文件路径</label>' +
|
||||||
|
'<input class="pipeline-form-input" id="pf_audio" placeholder="/path/to/audio.mp3" /></div>' +
|
||||||
|
|
||||||
|
'<div class="pipeline-form-group" id="pf_video_group" style="display:none">' +
|
||||||
|
'<label class="pipeline-form-label">视频文件路径</label>' +
|
||||||
|
'<input class="pipeline-form-input" id="pf_video" placeholder="/path/to/video.mp4" /></div>' +
|
||||||
|
|
||||||
|
'<div class="pipeline-form-group">' +
|
||||||
|
'<label class="pipeline-form-label">歌曲名称</label>' +
|
||||||
|
'<input class="pipeline-form-input" id="pf_title" placeholder="输入歌曲名称" /></div>' +
|
||||||
|
|
||||||
|
'<div class="pipeline-form-group" id="pf_lyrics_group">' +
|
||||||
|
'<label class="pipeline-form-label">歌词</label>' +
|
||||||
|
'<textarea class="pipeline-form-textarea" id="pf_lyrics" placeholder="输入歌词文本,或留空由AI生成"></textarea></div>' +
|
||||||
|
|
||||||
|
'<div class="pipeline-form-group">' +
|
||||||
|
'<label class="pipeline-form-label">词作者</label>' +
|
||||||
|
'<input class="pipeline-form-input" id="pf_lyricist" placeholder="可选" /></div>' +
|
||||||
|
|
||||||
|
'<div class="pipeline-form-group">' +
|
||||||
|
'<label class="pipeline-form-label">曲作者</label>' +
|
||||||
|
'<input class="pipeline-form-input" id="pf_composer" placeholder="可选" /></div>' +
|
||||||
|
|
||||||
|
'<div style="margin-top:24px">' +
|
||||||
|
'<button class="pipeline-btn pipeline-btn-primary" onclick="PipelineUI.submitTask()" id="pf_submit_btn">提交任务</button>' +
|
||||||
|
'<button class="pipeline-btn pipeline-btn-secondary" onclick="PipelineUI.showListView()" style="margin-left:8px">取消</button>' +
|
||||||
|
'</div></div>' +
|
||||||
|
'<div id="pipeline_create_msg"></div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onModeChange() {
|
||||||
|
var mode = document.getElementById('pf_mode').value;
|
||||||
|
var audioG = document.getElementById('pf_audio_group');
|
||||||
|
var videoG = document.getElementById('pf_video_group');
|
||||||
|
var lyricsG = document.getElementById('pf_lyrics_group');
|
||||||
|
if (audioG) audioG.style.display = (mode === 'audio_lyrics') ? 'block' : 'none';
|
||||||
|
if (videoG) videoG.style.display = (mode === 'video_lyrics') ? 'block' : 'none';
|
||||||
|
if (lyricsG) lyricsG.style.display = (mode === 'video_lyrics') ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitTask() {
|
||||||
|
var mode = document.getElementById('pf_mode').value;
|
||||||
|
var title = document.getElementById('pf_title').value;
|
||||||
|
var lyrics = document.getElementById('pf_lyrics') ? document.getElementById('pf_lyrics').value : '';
|
||||||
|
var lyricist = document.getElementById('pf_lyricist').value;
|
||||||
|
var composer = document.getElementById('pf_composer').value;
|
||||||
|
|
||||||
|
if (!title) { alert('请输入歌曲名称'); return; }
|
||||||
|
|
||||||
|
var body = { mode: mode, title: title, lyrics: lyrics, lyricist: lyricist, composer: composer };
|
||||||
|
|
||||||
|
if (mode === 'audio_lyrics') {
|
||||||
|
body.input_audio = document.getElementById('pf_audio').value;
|
||||||
|
if (!body.input_audio) { alert('请输入音频文件路径'); return; }
|
||||||
|
} else if (mode === 'video_lyrics') {
|
||||||
|
body.input_video = document.getElementById('pf_video').value;
|
||||||
|
if (!body.input_video) { alert('请输入视频文件路径'); return; }
|
||||||
|
}
|
||||||
|
|
||||||
|
var btn = document.getElementById('pf_submit_btn');
|
||||||
|
if (btn) { btn.disabled = true; btn.textContent = '提交中...'; }
|
||||||
|
|
||||||
|
apiPost('/api/pipeline_submit.dspy', body, function(err, data) {
|
||||||
|
if (btn) { btn.disabled = false; btn.textContent = '提交任务'; }
|
||||||
|
if (err) { alert('提交失败: ' + err); return; }
|
||||||
|
if (data.status === 'error') { alert('提交失败: ' + (data.message || '未知错误')); return; }
|
||||||
|
|
||||||
|
var pid = data.pipeline_id;
|
||||||
|
if (pid) {
|
||||||
|
showDetailView(pid);
|
||||||
|
} else {
|
||||||
|
showListView();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Task Detail View ===
|
||||||
|
|
||||||
|
function showDetailView(pipelineId) {
|
||||||
|
currentView = 'detail';
|
||||||
|
currentPipelineId = pipelineId;
|
||||||
|
var c = getContainer();
|
||||||
|
if (!c) return;
|
||||||
|
c.innerHTML = '<div class="pipeline-container">' +
|
||||||
|
'<div class="pipeline-detail-header">' +
|
||||||
|
'<button class="pipeline-back-btn" onclick="PipelineUI.showListView()">← 返回</button>' +
|
||||||
|
'<div><div class="pipeline-detail-title" id="pd_title">加载中...</div>' +
|
||||||
|
'<div class="pipeline-detail-meta" id="pd_meta"></div></div></div>' +
|
||||||
|
'<div id="pd_nodes"><div class="pipeline-loading">加载任务节点...</div></div>' +
|
||||||
|
'</div>';
|
||||||
|
loadTaskDetail(pipelineId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTaskDetail(pid) {
|
||||||
|
apiGet('/api/pipeline_detail.dspy?pipeline_id=' + encodeURIComponent(pid), function(err, data) {
|
||||||
|
if (err) {
|
||||||
|
document.getElementById('pd_nodes').innerHTML = '<div class="pipeline-error">' + err + '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.status === 'error') {
|
||||||
|
document.getElementById('pd_nodes').innerHTML = '<div class="pipeline-error">' + (data.message || '加载失败') + '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var titleEl = document.getElementById('pd_title');
|
||||||
|
var metaEl = document.getElementById('pd_meta');
|
||||||
|
var nodesEl = document.getElementById('pd_nodes');
|
||||||
|
|
||||||
|
if (titleEl) titleEl.textContent = data.title || pid;
|
||||||
|
if (metaEl) {
|
||||||
|
var st = data.state || '';
|
||||||
|
var stText = {completed:'已完成', running:'运行中', failed:'失败', submitted:'已提交'}[st] || st;
|
||||||
|
metaEl.innerHTML = 'ID: ' + esc(pid) + ' | ' +
|
||||||
|
esc(data.mode || '') + ' | ' +
|
||||||
|
'<span class="pipeline-status status-' + st + '">' + stText + '</span>' +
|
||||||
|
' | 版本 v' + (data.current_version || 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNodeTree(nodesEl, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNodeTree(container, data) {
|
||||||
|
if (!container) return;
|
||||||
|
var steps = data.steps || {};
|
||||||
|
var artifacts = data.artifacts || {};
|
||||||
|
var versions = data.versions || {};
|
||||||
|
|
||||||
|
// Sort steps by order
|
||||||
|
var stepList = [];
|
||||||
|
for (var name in steps) {
|
||||||
|
if (steps.hasOwnProperty(name)) {
|
||||||
|
var s = steps[name];
|
||||||
|
stepList.push({name: name, order: s.order || 0, version: s.version || 1, deps: s.deps || [], state: s.state || ''});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stepList.sort(function(a, b) { return a.order - b.order; });
|
||||||
|
|
||||||
|
if (!stepList.length) {
|
||||||
|
container.innerHTML = '<div class="pipeline-empty">无步骤数据</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '<div class="pipeline-node-tree">';
|
||||||
|
for (var i = 0; i < stepList.length; i++) {
|
||||||
|
var step = stepList[i];
|
||||||
|
var st = step.state || 'pending';
|
||||||
|
var stText = {completed:'✓', running:'⟳', pending:'○', failed:'✗'}[st] || '?';
|
||||||
|
var iconClass = 'node-icon-' + st;
|
||||||
|
var nameCN = STEP_NAMES[step.name] || step.name;
|
||||||
|
|
||||||
|
// Find artifact for this step
|
||||||
|
var artKey = '';
|
||||||
|
for (var ak in artifacts) {
|
||||||
|
if (artifacts.hasOwnProperty(ak) && artifacts[ak].step === step.name) {
|
||||||
|
artKey = ak;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '<div class="pipeline-node-card" id="node_' + step.name + '">' +
|
||||||
|
'<div class="pipeline-node-header" onclick="PipelineUI.toggleNode(\'' + step.name + '\')">' +
|
||||||
|
'<div class="pipeline-node-icon ' + iconClass + '">' + stText + '</div>' +
|
||||||
|
'<span class="pipeline-node-name">' + esc(nameCN) + '</span>' +
|
||||||
|
'<span class="pipeline-node-version">v' + step.version + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="pipeline-node-body" id="node_body_' + step.name + '">';
|
||||||
|
|
||||||
|
if (st === 'completed') {
|
||||||
|
html += '<div class="pipeline-io-section">' +
|
||||||
|
'<div class="pipeline-io-label">输入 / 输出</div>' +
|
||||||
|
'<div id="node_io_' + step.name + '" class="pipeline-io-content">点击加载...</div>' +
|
||||||
|
'<div class="pipeline-io-actions">' +
|
||||||
|
'<button class="pipeline-btn pipeline-btn-secondary pipeline-btn-sm" onclick="PipelineUI.loadNodeIO(\'' + step.name + '\')">刷新</button>' +
|
||||||
|
'<button class="pipeline-btn pipeline-btn-primary pipeline-btn-sm" onclick="PipelineUI.showNodeEdit(\'' + step.name + '\', \'input\')">修改输入</button>' +
|
||||||
|
'<button class="pipeline-btn pipeline-btn-primary pipeline-btn-sm" onclick="PipelineUI.showNodeEdit(\'' + step.name + '\', \'output\')">修改输出</button>' +
|
||||||
|
'</div></div>';
|
||||||
|
} else if (st === 'running') {
|
||||||
|
html += '<div class="pipeline-loading">该节点正在运行中...</div>';
|
||||||
|
} else if (st === 'failed') {
|
||||||
|
html += '<div class="pipeline-error">该节点执行失败</div>';
|
||||||
|
} else {
|
||||||
|
html += '<div style="color:#aaa;font-size:13px">等待执行</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div></div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
// Auto-expand failed or running node
|
||||||
|
for (var j = 0; j < stepList.length; j++) {
|
||||||
|
if (stepList[j].state === 'failed' || stepList[j].state === 'running') {
|
||||||
|
toggleNode(stepList[j].name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step name Chinese mapping
|
||||||
|
var STEP_NAMES = {
|
||||||
|
'audio_preparing': '音频准备',
|
||||||
|
'video_preparing': '视频准备',
|
||||||
|
'demucs_separating': '人声分离',
|
||||||
|
'lyric_generating': '歌词生成',
|
||||||
|
'lyric_evaluating': '歌词评估',
|
||||||
|
'music_generating': '音乐生成',
|
||||||
|
'music_polling': '音乐轮询',
|
||||||
|
'lyric_calibrating': '歌词校准',
|
||||||
|
'subtitle_rendering': '字幕渲染',
|
||||||
|
'subtitle_exporting': '字幕导出',
|
||||||
|
'character_designing': '角色设计',
|
||||||
|
'character_image_generating': '角色图生成',
|
||||||
|
'storyboard_generating': '分镜剧本',
|
||||||
|
'scene_video_generating': '分镜视频生成',
|
||||||
|
'scene_video_evaluating': '分镜视频评估',
|
||||||
|
'scene_video_concatenating': '分镜视频拼接',
|
||||||
|
'ktv_synthesizing': 'KTV合成'
|
||||||
|
};
|
||||||
|
|
||||||
|
function toggleNode(stepName) {
|
||||||
|
var body = document.getElementById('node_body_' + stepName);
|
||||||
|
if (!body) return;
|
||||||
|
var isExpanded = body.classList.contains('expanded');
|
||||||
|
body.classList.toggle('expanded');
|
||||||
|
|
||||||
|
// Load IO data on first expand
|
||||||
|
if (!isExpanded) {
|
||||||
|
var ioEl = document.getElementById('node_io_' + stepName);
|
||||||
|
if (ioEl && ioEl.textContent === '点击加载...') {
|
||||||
|
loadNodeIO(stepName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadNodeIO(stepName) {
|
||||||
|
var ioEl = document.getElementById('node_io_' + stepName);
|
||||||
|
if (!ioEl) return;
|
||||||
|
ioEl.textContent = '加载中...';
|
||||||
|
|
||||||
|
apiGet('/api/pipeline_node.dspy?pipeline_id=' + encodeURIComponent(currentPipelineId) + '&step=' + encodeURIComponent(stepName), function(err, data) {
|
||||||
|
if (err) { ioEl.textContent = '加载失败: ' + err; return; }
|
||||||
|
if (data.status === 'error') { ioEl.textContent = '错误: ' + (data.message || ''); return; }
|
||||||
|
|
||||||
|
var text = '';
|
||||||
|
if (data.input) {
|
||||||
|
text += '【输入】\n' + formatData(data.input) + '\n\n';
|
||||||
|
}
|
||||||
|
if (data.output) {
|
||||||
|
text += '【输出】\n' + formatData(data.output);
|
||||||
|
}
|
||||||
|
if (!text) text = '(无数据)';
|
||||||
|
ioEl.textContent = text;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Node Edit ===
|
||||||
|
|
||||||
|
function showNodeEdit(stepName, modifyType) {
|
||||||
|
var nodeBody = document.getElementById('node_body_' + stepName);
|
||||||
|
if (!nodeBody) return;
|
||||||
|
|
||||||
|
// Check if edit section already exists
|
||||||
|
var existing = document.getElementById('node_edit_' + stepName);
|
||||||
|
if (existing) { existing.remove(); return; }
|
||||||
|
|
||||||
|
var typeLabel = modifyType === 'input' ? '修改输入(从该节点重跑)' : '修改输出(从下一节点重跑)';
|
||||||
|
|
||||||
|
// Get current content
|
||||||
|
var ioEl = document.getElementById('node_io_' + stepName);
|
||||||
|
var currentContent = '';
|
||||||
|
if (ioEl) {
|
||||||
|
var text = ioEl.textContent;
|
||||||
|
if (modifyType === 'input' && text.indexOf('【输入】') >= 0) {
|
||||||
|
currentContent = text.substring(text.indexOf('【输入】') + 4, text.indexOf('【输出】') >= 0 ? text.indexOf('【输出】') : text.length).trim();
|
||||||
|
} else if (modifyType === 'output' && text.indexOf('【输出】') >= 0) {
|
||||||
|
currentContent = text.substring(text.indexOf('【输出】') + 4).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '<div class="pipeline-edit-section" id="node_edit_' + stepName + '">' +
|
||||||
|
'<div class="pipeline-io-label">' + esc(typeLabel) + '</div>' +
|
||||||
|
'<textarea class="pipeline-edit-textarea" id="node_edit_content_' + stepName + '">' + esc(currentContent) + '</textarea>' +
|
||||||
|
'<input type="hidden" id="node_edit_type_' + stepName + '" value="' + modifyType + '" />' +
|
||||||
|
'<div class="pipeline-edit-actions">' +
|
||||||
|
'<button class="pipeline-btn pipeline-btn-primary pipeline-btn-sm" onclick="PipelineUI.submitNodeEdit(\'' + stepName + '\')" id="node_edit_btn_' + stepName + '">确认修改并重跑</button>' +
|
||||||
|
'<button class="pipeline-btn pipeline-btn-secondary pipeline-btn-sm" onclick="document.getElementById(\'node_edit_' + stepName + '\').remove()">取消</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div id="node_edit_msg_' + stepName + '"></div></div>';
|
||||||
|
|
||||||
|
nodeBody.insertAdjacentHTML('beforeend', html);
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitNodeEdit(stepName) {
|
||||||
|
var content = document.getElementById('node_edit_content_' + stepName).value;
|
||||||
|
var modifyType = document.getElementById('node_edit_type_' + stepName).value;
|
||||||
|
var msgEl = document.getElementById('node_edit_msg_' + stepName);
|
||||||
|
var btn = document.getElementById('node_edit_btn_' + stepName);
|
||||||
|
|
||||||
|
if (!content.trim()) { alert('内容不能为空'); return; }
|
||||||
|
|
||||||
|
if (btn) { btn.disabled = true; btn.textContent = '提交中...'; }
|
||||||
|
if (msgEl) msgEl.innerHTML = '<div class="pipeline-loading">提交修改...</div>';
|
||||||
|
|
||||||
|
apiPost('/api/pipeline_modify.dspy', {
|
||||||
|
pipeline_id: currentPipelineId,
|
||||||
|
step: stepName,
|
||||||
|
modify_type: modifyType,
|
||||||
|
content: content
|
||||||
|
}, function(err, data) {
|
||||||
|
if (btn) { btn.disabled = false; btn.textContent = '确认修改并重跑'; }
|
||||||
|
if (err) {
|
||||||
|
if (msgEl) msgEl.innerHTML = '<div class="pipeline-error">' + err + '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.status === 'error') {
|
||||||
|
if (msgEl) msgEl.innerHTML = '<div class="pipeline-error">' + (data.message || '修改失败') + '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgEl) msgEl.innerHTML = '<div style="color:green;font-size:13px;margin-top:8px">修改成功!' +
|
||||||
|
(data.new_version ? ' 新版本: v' + data.new_version : '') +
|
||||||
|
(data.rerun_steps ? ' 重跑步骤: ' + data.rerun_steps.join(', ') : '') +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
// Reload task detail after 2 seconds
|
||||||
|
setTimeout(function() { loadTaskDetail(currentPipelineId); }, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Utilities ===
|
||||||
|
|
||||||
|
function formatData(obj) {
|
||||||
|
if (typeof obj === 'string') return obj;
|
||||||
|
try { return JSON.stringify(obj, null, 2); } catch(e) { return String(obj); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
if (!s) return '';
|
||||||
|
var d = document.createElement('div');
|
||||||
|
d.appendChild(document.createTextNode(s));
|
||||||
|
return d.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Expose Public API ===
|
||||||
|
|
||||||
|
window.PipelineUI = {
|
||||||
|
init: init,
|
||||||
|
showListView: showListView,
|
||||||
|
showCreateView: showCreateView,
|
||||||
|
showDetailView: showDetailView,
|
||||||
|
submitTask: submitTask,
|
||||||
|
onModeChange: onModeChange,
|
||||||
|
toggleNode: toggleNode,
|
||||||
|
loadNodeIO: loadNodeIO,
|
||||||
|
showNodeEdit: showNodeEdit,
|
||||||
|
submitNodeEdit: submitNodeEdit
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-init on DOM ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
Loading…
x
Reference in New Issue
Block a user