From 50a38e15e1760a12ca0c75e345f521968fd78738 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Thu, 11 Jun 2026 17:34:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20pipeline-task=20=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=88=9D=E5=A7=8B=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 纯薄交互层,无数据表,调用 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 文档 --- .gitignore | 21 ++++++++++ pipeline_task/__init__.py | 5 +++ pipeline_task/init.py | 22 +++++++++++ pyproject.toml | 16 ++++++++ scripts/load_path.py | 63 +++++++++++++++++++++++++++++ wwwroot/api/task_control.dspy | 20 ++++++++++ wwwroot/api/task_detail.dspy | 11 ++++++ wwwroot/api/task_list.dspy | 9 +++++ wwwroot/api/task_modify.dspy | 25 ++++++++++++ wwwroot/api/task_node.dspy | 13 ++++++ wwwroot/api/task_submit.dspy | 22 +++++++++++ wwwroot/index.ui | 28 +++++++++++++ wwwroot/pipeline_task.js | 74 +++++++++++++++++++++++++++++++++++ wwwroot/task_detail.ui | 73 ++++++++++++++++++++++++++++++++++ wwwroot/task_list.ui | 42 ++++++++++++++++++++ wwwroot/task_submit.ui | 59 ++++++++++++++++++++++++++++ 16 files changed, 503 insertions(+) create mode 100644 .gitignore create mode 100644 pipeline_task/__init__.py create mode 100644 pipeline_task/init.py create mode 100644 pyproject.toml create mode 100644 scripts/load_path.py create mode 100644 wwwroot/api/task_control.dspy create mode 100644 wwwroot/api/task_detail.dspy create mode 100644 wwwroot/api/task_list.dspy create mode 100644 wwwroot/api/task_modify.dspy create mode 100644 wwwroot/api/task_node.dspy create mode 100644 wwwroot/api/task_submit.dspy create mode 100644 wwwroot/index.ui create mode 100644 wwwroot/pipeline_task.js create mode 100644 wwwroot/task_detail.ui create mode 100644 wwwroot/task_list.ui create mode 100644 wwwroot/task_submit.ui diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..403bf6e --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/pipeline_task/__init__.py b/pipeline_task/__init__.py new file mode 100644 index 0000000..94fec18 --- /dev/null +++ b/pipeline_task/__init__.py @@ -0,0 +1,5 @@ +"""pipeline_task — 产线任务交互层""" + +from .init import load_pipeline_task + +__version__ = "1.0.0" diff --git a/pipeline_task/init.py b/pipeline_task/init.py new file mode 100644 index 0000000..f80180f --- /dev/null +++ b/pipeline_task/init.py @@ -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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a1f66c7 --- /dev/null +++ b/pyproject.toml @@ -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*"] diff --git a/scripts/load_path.py b/scripts/load_path.py new file mode 100644 index 0000000..91c20c5 --- /dev/null +++ b/scripts/load_path.py @@ -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.") diff --git a/wwwroot/api/task_control.dspy b/wwwroot/api/task_control.dspy new file mode 100644 index 0000000..f491452 --- /dev/null +++ b/wwwroot/api/task_control.dspy @@ -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) diff --git a/wwwroot/api/task_detail.dspy b/wwwroot/api/task_detail.dspy new file mode 100644 index 0000000..6baa232 --- /dev/null +++ b/wwwroot/api/task_detail.dspy @@ -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) diff --git a/wwwroot/api/task_list.dspy b/wwwroot/api/task_list.dspy new file mode 100644 index 0000000..64b7271 --- /dev/null +++ b/wwwroot/api/task_list.dspy @@ -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) diff --git a/wwwroot/api/task_modify.dspy b/wwwroot/api/task_modify.dspy new file mode 100644 index 0000000..73c4bb9 --- /dev/null +++ b/wwwroot/api/task_modify.dspy @@ -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) diff --git a/wwwroot/api/task_node.dspy b/wwwroot/api/task_node.dspy new file mode 100644 index 0000000..48d3847 --- /dev/null +++ b/wwwroot/api/task_node.dspy @@ -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) diff --git a/wwwroot/api/task_submit.dspy b/wwwroot/api/task_submit.dspy new file mode 100644 index 0000000..6cf8d81 --- /dev/null +++ b/wwwroot/api/task_submit.dspy @@ -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) diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..98e08b8 --- /dev/null +++ b/wwwroot/index.ui @@ -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"} + } + ] +} diff --git a/wwwroot/pipeline_task.js b/wwwroot/pipeline_task.js new file mode 100644 index 0000000..ab6b8b3 --- /dev/null +++ b/wwwroot/pipeline_task.js @@ -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('
' + (data.message || '加载失败') + '
'); + return; + } + + var tasks = data.tasks || []; + if (tasks.length === 0) { + $('#task_table_area').html('
暂无任务
'); + return; + } + + var html = ''; + html += ''; + html += ''; + + tasks.forEach(function(t) { + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + }); + + html += '
任务ID标题状态版本创建时间操作
' + (t.id || '').substring(0, 8) + '' + (t.title || '') + '' + (t.state || '') + 'v' + (t.current_version || 1) + '' + (t.created_at || '') + '
'; + $('#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(); + } +}); diff --git a/wwwroot/task_detail.ui b/wwwroot/task_detail.ui new file mode 100644 index 0000000..42611da --- /dev/null +++ b/wwwroot/task_detail.ui @@ -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": "点击左侧步骤查看产物"}} + ] + } + ] + } + ] +} diff --git a/wwwroot/task_list.ui b/wwwroot/task_list.ui new file mode 100644 index 0000000..a4deb06 --- /dev/null +++ b/wwwroot/task_list.ui @@ -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": "加载中..."}} + ] + } + ] +} diff --git a/wwwroot/task_submit.ui b/wwwroot/task_submit.ui new file mode 100644 index 0000000..35b2f6f --- /dev/null +++ b/wwwroot/task_submit.ui @@ -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"} + } + ] + } + ] + } + ] +}