feat: pipeline-task 交互模块初始版本

- 纯薄交互层,无数据表,调用 pipeline-service 引擎函数
- 6个dspy API(submit/list/detail/node/modify/control)
- 4个UI页面(index/list/detail/submit)
- pipeline_task.js 交互辅助函数
- load_path.py RBAC 权限注册
- 完整 README 文档
This commit is contained in:
yumoqing 2026-06-11 17:34:50 +08:00
parent d7bde8ec31
commit 50a38e15e1
16 changed files with 503 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
dist/
build/
*.egg
# Virtual environment
py3/
pkgs/
# IDE
.vscode/
.idea/
# OS
.DS_Store
# Logs
logs/

View File

@ -0,0 +1,5 @@
"""pipeline_task — 产线任务交互层"""
from .init import load_pipeline_task
__version__ = "1.0.0"

22
pipeline_task/init.py Normal file
View File

@ -0,0 +1,22 @@
"""pipeline_task 交互模块 — 产线任务的用户交互层。
纯薄交互层无数据表通过 ServerEnv 调用 pipeline-service 的函数
任何宿主加载 pipeline_service 再加载 pipeline_task 即可使用
"""
from ahserver.serverenv import ServerEnv
MODULE_NAME = "pipeline_task"
MODULE_VERSION = "1.0.0"
def load_pipeline_task():
"""注册交互模块到 ServerEnv。"""
env = ServerEnv()
# 模块元信息
env.pipeline_task_info = lambda: {
"module": MODULE_NAME,
"version": MODULE_VERSION,
"depends_on": "pipeline_service",
}
return True

16
pyproject.toml Normal file
View File

@ -0,0 +1,16 @@
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "pipeline_task"
version = "1.0.0"
description = "产线任务交互模块 — 提交任务、查看状态、修改节点、暂停恢复"
requires-python = ">=3.8"
dependencies = [
"bricks_for_python",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["pipeline_task*"]

63
scripts/load_path.py Normal file
View File

@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""pipeline_task 模块 RBAC 权限注册。"""
import os
import sys
import subprocess
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
# Find Sage root
SAGE_ROOT = None
for candidate in [
os.path.join(SCRIPT_DIR, "..", ".."),
os.path.expanduser("~/repos/sage"),
os.path.expanduser("~/sage"),
os.path.expanduser("~/test/pipeline-app"),
]:
if os.path.isdir(os.path.join(candidate, "wwwroot")) and os.path.isdir(os.path.join(candidate, "py3")):
SAGE_ROOT = os.path.abspath(candidate)
break
if not SAGE_ROOT:
print("ERROR: Cannot find Sage/pipeline-app root directory")
sys.exit(1)
SET_ROLE_PERM = os.path.join(SAGE_ROOT, "set_role_perm.py")
PYTHON = os.path.join(SAGE_ROOT, "py3", "bin", "python")
MOD = "pipeline_task"
PATHS_LOGINED = [
f"/{MOD}/index.ui",
f"/{MOD}/task_list.ui",
f"/{MOD}/task_detail.ui",
f"/{MOD}/task_submit.ui",
f"/{MOD}/api/task_submit.dspy",
f"/{MOD}/api/task_list.dspy",
f"/{MOD}/api/task_detail.dspy",
f"/{MOD}/api/task_node.dspy",
f"/{MOD}/api/task_modify.dspy",
f"/{MOD}/api/task_control.dspy",
]
PATHS_ANY = [
f"/{MOD}/pipeline_task.js",
]
def register_paths():
for path in PATHS_ANY:
subprocess.run([PYTHON, SET_ROLE_PERM, "any", path], cwd=SAGE_ROOT)
print(f" any: {path}")
for path in PATHS_LOGINED:
subprocess.run([PYTHON, SET_ROLE_PERM, "logined", path], cwd=SAGE_ROOT)
print(f" logined: {path}")
if __name__ == "__main__":
print(f"=== pipeline_task RBAC registration ===")
print(f"Root: {SAGE_ROOT}")
register_paths()
print("Done.")

View File

@ -0,0 +1,20 @@
tenant_id = (await get_userorgid()) or '0'
task_id = params_kw.get('task_id', '')
action = params_kw.get('action', '')
if not task_id:
return json.dumps({"success": False, "message": "缺少task_id"}, ensure_ascii=False)
if action not in ('pause', 'resume', 'cancel'):
return json.dumps({"success": False, "message": "action必须是pause/resume/cancel"}, ensure_ascii=False)
try:
if action == 'pause':
result = await pipeline_pause(tenant_id, task_id)
elif action == 'resume':
result = await pipeline_resume(tenant_id, task_id)
else:
result = await pipeline_cancel(tenant_id, task_id)
return result
except Exception as e:
return json.dumps({"success": False, "message": str(e)}, ensure_ascii=False)

View File

@ -0,0 +1,11 @@
tenant_id = (await get_userorgid()) or '0'
task_id = params_kw.get('task_id', '')
if not task_id:
return json.dumps({"success": False, "message": "缺少task_id"}, ensure_ascii=False)
try:
result = await pipeline_detail(tenant_id, task_id)
return result
except Exception as e:
return json.dumps({"success": False, "message": str(e)}, ensure_ascii=False)

View File

@ -0,0 +1,9 @@
tenant_id = (await get_userorgid()) or '0'
pipeline_id = params_kw.get('pipeline_id', None)
limit = int(params_kw.get('limit', 100))
try:
result = await pipeline_list(tenant_id, pipeline_id, limit)
return result
except Exception as e:
return json.dumps({"success": False, "message": str(e)}, ensure_ascii=False)

View File

@ -0,0 +1,25 @@
tenant_id = (await get_userorgid()) or '0'
task_id = params_kw.get('task_id', '')
step_name = params_kw.get('step_name', '')
content_raw = params_kw.get('content', '')
rerun_from = params_kw.get('rerun_from', 'node')
if not task_id or not step_name:
return json.dumps({"success": False, "message": "缺少task_id或step_name"}, ensure_ascii=False)
# Parse content
if isinstance(content_raw, str):
try:
content = json.loads(content_raw)
except Exception:
content = {"text": content_raw}
else:
content = content_raw
updates = {step_name: {"content": content}}
try:
result = await pipeline_modify(tenant_id, task_id, updates, rerun_from)
return result
except Exception as e:
return json.dumps({"success": False, "message": str(e)}, ensure_ascii=False)

View File

@ -0,0 +1,13 @@
tenant_id = (await get_userorgid()) or '0'
task_id = params_kw.get('task_id', '')
step_name = params_kw.get('step_name', '')
version = params_kw.get('version', None)
if not task_id or not step_name:
return json.dumps({"success": False, "message": "缺少task_id或step_name"}, ensure_ascii=False)
try:
result = await pipeline_node(tenant_id, task_id, step_name, version)
return result
except Exception as e:
return json.dumps({"success": False, "message": str(e)}, ensure_ascii=False)

View File

@ -0,0 +1,22 @@
tenant_id = (await get_userorgid()) or '0'
pipeline_id = params_kw.get('pipeline_id', '')
title = params_kw.get('title', '')
owner_id = await get_user()
if not pipeline_id:
return json.dumps({"success": False, "message": "缺少产线ID"}, ensure_ascii=False)
if not title:
return json.dumps({"success": False, "message": "缺少任务标题"}, ensure_ascii=False)
# Collect optional params
task_params = {}
for key in ['input_audio', 'input_video', 'input_text', 'lyrics', 'mode', 'scene']:
val = params_kw.get(key, '')
if val:
task_params[key] = val
try:
result = await pipeline_submit(tenant_id, pipeline_id, owner_id, title, task_params)
return result
except Exception as e:
return json.dumps({"success": False, "message": str(e)}, ensure_ascii=False)

28
wwwroot/index.ui Normal file
View File

@ -0,0 +1,28 @@
{
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "20px"},
"subwidgets": [
{
"widgettype": "HBox",
"options": {"width": "100%", "justifyContent": "space-between", "alignItems": "center", "marginBottom": "16px"},
"subwidgets": [
{"widgettype": "Title", "options": {"text": "产线任务", "cfontsize": 24}},
{
"widgettype": "Button",
"options": {"label": "提交新任务", "icon": "plus"},
"binds": [{
"wid": "self", "event": "click", "actiontype": "urlwidget",
"target": "app.pipeline_task_content",
"options": {"url": "{{entire_url('task_submit.ui')}}"},
"mode": "replace"
}]
}
]
},
{
"widgettype": "VBox",
"id": "pipeline_task_content",
"options": {"width": "100%", "flex": "1"}
}
]
}

74
wwwroot/pipeline_task.js Normal file
View File

@ -0,0 +1,74 @@
// pipeline_task.js — 产线任务交互辅助函数
var currentTaskId = '';
// 加载任务列表
function loadTaskList() {
var filterPipeline = $('[name="filter_pipeline"]').val() || '';
var url = entire_url('api/task_list.dspy');
if (filterPipeline) {
url += '?pipeline_id=' + filterPipeline;
}
$.get(url, function(resp) {
var data = typeof resp === 'string' ? JSON.parse(resp) : resp;
if (!data.success) {
$('#task_table_area').html('<div class="alert error">' + (data.message || '加载失败') + '</div>');
return;
}
var tasks = data.tasks || [];
if (tasks.length === 0) {
$('#task_table_area').html('<div class="empty-state">暂无任务</div>');
return;
}
var html = '<table class="data-table"><thead><tr>';
html += '<th>任务ID</th><th>标题</th><th>状态</th><th>版本</th><th>创建时间</th><th>操作</th>';
html += '</tr></thead><tbody>';
tasks.forEach(function(t) {
html += '<tr>';
html += '<td>' + (t.id || '').substring(0, 8) + '</td>';
html += '<td>' + (t.title || '') + '</td>';
html += '<td><span class="badge state-' + (t.state || '') + '">' + (t.state || '') + '</span></td>';
html += '<td>v' + (t.current_version || 1) + '</td>';
html += '<td>' + (t.created_at || '') + '</td>';
html += '<td><button class="btn-small" onclick="viewTask(\'' + t.id + '\')">查看</button></td>';
html += '</tr>';
});
html += '</tbody></table>';
$('#task_table_area').html(html);
});
}
// 查看任务详情
function viewTask(taskId) {
currentTaskId = taskId;
var url = entire_url('task_detail.ui');
window.location.href = url + '?task_id=' + taskId;
}
// 控制任务(暂停/恢复/取消)
function controlTask(action) {
if (!currentTaskId) return;
var url = entire_url('api/task_control.dspy');
$.post(url, {task_id: currentTaskId, action: action}, function(resp) {
var data = typeof resp === 'string' ? JSON.parse(resp) : resp;
if (data.success) {
alert('操作成功: ' + data.message);
location.reload();
} else {
alert('操作失败: ' + (data.message || '未知错误'));
}
});
}
// 页面加载完成
$(function() {
if ($('#task_table_area').length > 0) {
loadTaskList();
}
});

73
wwwroot/task_detail.ui Normal file
View File

@ -0,0 +1,73 @@
{
"widgettype": "VBox",
"options": {"width": "100%", "padding": "16px"},
"subwidgets": [
{
"widgettype": "HBox",
"options": {"width": "100%", "marginBottom": "16px", "gap": "12px", "alignItems": "center"},
"subwidgets": [
{
"widgettype": "Button",
"options": {"label": "返回列表", "icon": "arrow-left"},
"binds": [{
"wid": "self", "event": "click", "actiontype": "urlwidget",
"target": "app.pipeline_task_content",
"options": {"url": "{{entire_url('task_list.ui')}}"},
"mode": "replace"
}]
},
{"widgettype": "Title", "options": {"text": "任务详情", "cfontsize": 20}},
{"widgettype": "Text", "options": {"text": "", "id": "detail_task_title"}},
{
"widgettype": "Button",
"options": {"label": "暂停", "icon": "pause", "id": "btn_pause"},
"binds": [{
"wid": "self", "event": "click", "actiontype": "script",
"target": "detail_steps_area",
"script": "controlTask('pause');"
}]
},
{
"widgettype": "Button",
"options": {"label": "恢复", "icon": "play", "id": "btn_resume"},
"binds": [{
"wid": "self", "event": "click", "actiontype": "script",
"target": "detail_steps_area",
"script": "controlTask('resume');"
}]
},
{
"widgettype": "Button",
"options": {"label": "取消", "icon": "stop", "id": "btn_cancel"},
"binds": [{
"wid": "self", "event": "click", "actiontype": "script",
"target": "detail_steps_area",
"script": "controlTask('cancel');"
}]
}
]
},
{
"widgettype": "HBox",
"options": {"width": "100%", "flex": "1", "gap": "16px"},
"subwidgets": [
{
"widgettype": "VBox",
"id": "detail_steps_area",
"options": {"width": "50%", "minHeight": "400px", "bgcolor": "var(--card-bg)", "padding": "16px"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "步骤列表加载中..."}}
]
},
{
"widgettype": "VBox",
"id": "detail_node_area",
"options": {"width": "50%", "minHeight": "400px", "bgcolor": "var(--card-bg)", "padding": "16px"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "点击左侧步骤查看产物"}}
]
}
]
}
]
}

42
wwwroot/task_list.ui Normal file
View File

@ -0,0 +1,42 @@
{
"widgettype": "VBox",
"options": {"width": "100%", "padding": "16px"},
"subwidgets": [
{
"widgettype": "HBox",
"options": {"width": "100%", "marginBottom": "16px", "gap": "12px"},
"subwidgets": [
{
"widgettype": "Input",
"options": {"name": "filter_pipeline", "placeholder": "按产线筛选", "width": "200px"}
},
{
"widgettype": "Button",
"options": {"label": "查询", "icon": "search"},
"binds": [{
"wid": "self", "event": "click", "actiontype": "script",
"target": "task_table_area",
"script": "loadTaskList();"
}]
},
{
"widgettype": "Button",
"options": {"label": "刷新", "icon": "refresh"},
"binds": [{
"wid": "self", "event": "click", "actiontype": "script",
"target": "task_table_area",
"script": "loadTaskList();"
}]
}
]
},
{
"widgettype": "VBox",
"id": "task_table_area",
"options": {"width": "100%", "flex": "1", "minHeight": "400px", "bgcolor": "var(--card-bg)", "padding": "16px"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "加载中..."}}
]
}
]
}

59
wwwroot/task_submit.ui Normal file
View File

@ -0,0 +1,59 @@
{
"widgettype": "VBox",
"options": {"width": "100%", "padding": "16px"},
"subwidgets": [
{
"widgettype": "HBox",
"options": {"width": "100%", "marginBottom": "16px", "alignItems": "center"},
"subwidgets": [
{
"widgettype": "Button",
"options": {"label": "返回列表", "icon": "arrow-left"},
"binds": [{
"wid": "self", "event": "click", "actiontype": "urlwidget",
"target": "app.pipeline_task_content",
"options": {"url": "{{entire_url('task_list.ui')}}"},
"mode": "replace"
}]
},
{"widgettype": "Title", "options": {"text": "提交新任务", "cfontsize": 20}}
]
},
{
"widgettype": "VBox",
"options": {"width": "600px", "bgcolor": "var(--card-bg)", "padding": "24px"},
"subwidgets": [
{
"widgettype": "Form",
"options": {
"name": "submit_form",
"url": "{{entire_url('api/task_submit.dspy')}}",
"method": "POST"
},
"subwidgets": [
{
"widgettype": "Input",
"options": {"name": "pipeline_id", "label": "产线ID", "required": true, "width": "100%"}
},
{
"widgettype": "Input",
"options": {"name": "title", "label": "任务标题", "required": true, "width": "100%"}
},
{
"widgettype": "Input",
"options": {"name": "mode", "label": "模式(可选)", "width": "100%"}
},
{
"widgettype": "TextArea",
"options": {"name": "input_text", "label": "输入文本(可选)", "width": "100%", "rows": 6}
},
{
"widgettype": "Button",
"options": {"label": "提交", "actiontype": "method", "method": "submit"}
}
]
}
]
}
]
}