From b556ab0cdce2f5c10039bbdb102816aa507d2e0a Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 8 Jul 2026 21:03:32 +0800 Subject: [PATCH] feat: task center - human task interaction, artifact view, restart capability - Add human task dspy: human_complete, human_list, approval_approve, approval_reject - Add task_restart.dspy for restarting completed/failed tasks - Enhance task_detail.ui: inline human task forms, approval buttons, restart - New step_panel.ui: view/modify step artifacts with cascade rerun - Update task_list.ui: Tabular with search_form filter by pipeline/state - Update index.ui: embed task list Tabular inline - Update load_path.py: register new dspy/ui paths --- pipeline_task/init.py | 42 ++++- scripts/load_path.py | 11 +- wwwroot/api/approval_approve.dspy | 14 ++ wwwroot/api/approval_reject.dspy | 15 ++ wwwroot/api/human_complete.dspy | 23 +++ wwwroot/api/human_list.dspy | 12 ++ wwwroot/api/task_list.dspy | 11 +- wwwroot/api/task_restart.dspy | 11 ++ wwwroot/index.ui | 89 +++++++++- wwwroot/step_panel.ui | 117 +++++++++++++ wwwroot/task_detail.ui | 263 +++++++++++++++++++++++++----- wwwroot/task_list.ui | 91 +++++++++-- 12 files changed, 632 insertions(+), 67 deletions(-) create mode 100644 wwwroot/api/approval_approve.dspy create mode 100644 wwwroot/api/approval_reject.dspy create mode 100644 wwwroot/api/human_complete.dspy create mode 100644 wwwroot/api/human_list.dspy create mode 100644 wwwroot/api/task_restart.dspy create mode 100644 wwwroot/step_panel.ui diff --git a/pipeline_task/init.py b/pipeline_task/init.py index f80180f..d5bf4c8 100644 --- a/pipeline_task/init.py +++ b/pipeline_task/init.py @@ -4,19 +4,57 @@ 任何宿主加载 pipeline_service 后,再加载 pipeline_task 即可使用。 """ +import json from ahserver.serverenv import ServerEnv MODULE_NAME = "pipeline_task" -MODULE_VERSION = "1.0.0" +MODULE_VERSION = "1.1.0" def load_pipeline_task(): """注册交互模块到 ServerEnv。""" env = ServerEnv() - # 模块元信息 env.pipeline_task_info = lambda: { "module": MODULE_NAME, "version": MODULE_VERSION, "depends_on": "pipeline_service", } + + # Jinja2 helper: preload task data for detail page rendering + async def get_task_data(request): + """Load task detail data for Jinja2 template rendering.""" + env_ns = request._run_ns + task_id = env_ns.params_kw.get('task_id', '') + if not task_id: + return {'success': False, 'message': '缺少task_id'} + + try: + tenant_id = (await env_ns.get_userorgid()) or '0' + result_raw = await env.pipeline_detail(tenant_id, task_id) + data = json.loads(result_raw) + return data + except Exception as e: + return {'success': False, 'message': str(e)} + + env.get_task_data = get_task_data + + # Jinja2 helper: preload step node artifact data + async def get_step_node_data(request): + """Load artifact data for step_panel Jinja2 template.""" + env_ns = request._run_ns + task_id = env_ns.params_kw.get('task_id', '') + step_name = env_ns.params_kw.get('step_name', '') + if not task_id or not step_name: + return {'success': False, 'message': '缺少参数'} + + try: + tenant_id = (await env_ns.get_userorgid()) or '0' + result_raw = await env.pipeline_node(tenant_id, task_id, step_name) + data = json.loads(result_raw) + return data + except Exception as e: + return {'success': False, 'message': str(e)} + + env.get_step_node_data = get_step_node_data + return True diff --git a/scripts/load_path.py b/scripts/load_path.py index eda5405..e08c94b 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -10,18 +10,21 @@ PATHS_LOGINED = [ f"/{MOD}/task_list.ui", f"/{MOD}/task_detail.ui", f"/{MOD}/task_submit.ui", + f"/{MOD}/step_panel.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", + f"/{MOD}/api/task_restart.dspy", + f"/{MOD}/api/human_complete.dspy", + f"/{MOD}/api/human_list.dspy", + f"/{MOD}/api/approval_approve.dspy", + f"/{MOD}/api/approval_reject.dspy", ] -# any — 无需登录(JS/CSS资源) -PATHS_ANY = [ - f"/{MOD}/pipeline_task.js", -] +PATHS_ANY = [] def register_paths(): diff --git a/wwwroot/api/approval_approve.dspy b/wwwroot/api/approval_approve.dspy new file mode 100644 index 0000000..4068628 --- /dev/null +++ b/wwwroot/api/approval_approve.dspy @@ -0,0 +1,14 @@ +tenant_id = (await get_userorgid()) or '0' +task_id = params_kw.get('task_id', '') +step_name = params_kw.get('step_name', '') +reviewer_id = await get_user() +comments = params_kw.get('comments', '') + +if not task_id or not step_name: + return json.dumps({"success": False, "message": "缺少task_id或step_name"}, ensure_ascii=False) + +try: + result = await approval_approve(tenant_id, task_id, step_name, reviewer_id, comments) + return result +except Exception as e: + return json.dumps({"success": False, "message": str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/approval_reject.dspy b/wwwroot/api/approval_reject.dspy new file mode 100644 index 0000000..a2ba491 --- /dev/null +++ b/wwwroot/api/approval_reject.dspy @@ -0,0 +1,15 @@ +tenant_id = (await get_userorgid()) or '0' +task_id = params_kw.get('task_id', '') +step_name = params_kw.get('step_name', '') +reviewer_id = await get_user() +comments = params_kw.get('comments', '') +rollback_to = params_kw.get('rollback_to', 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 approval_reject(tenant_id, task_id, step_name, reviewer_id, comments, rollback_to) + return result +except Exception as e: + return json.dumps({"success": False, "message": str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/human_complete.dspy b/wwwroot/api/human_complete.dspy new file mode 100644 index 0000000..fa73e51 --- /dev/null +++ b/wwwroot/api/human_complete.dspy @@ -0,0 +1,23 @@ +tenant_id = (await get_userorgid()) or '0' +task_id = params_kw.get('task_id', '') +step_name = params_kw.get('step_name', '') +operator_id = await get_user() + +if not task_id or not step_name: + return json.dumps({"success": False, "message": "缺少task_id或step_name"}, ensure_ascii=False) + +# Collect result data — form fields are passed as params_kw +# Ignore reserved keys +result_data = {} +for k, v in params_kw.items(): + if k not in ('task_id', 'step_name', '_', 'operator_id'): + result_data[k] = v + +if not result_data: + return json.dumps({"success": False, "message": "缺少表单数据"}, ensure_ascii=False) + +try: + result = await human_task_complete(tenant_id, task_id, step_name, result_data, operator_id) + return result +except Exception as e: + return json.dumps({"success": False, "message": str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/human_list.dspy b/wwwroot/api/human_list.dspy new file mode 100644 index 0000000..f9eef81 --- /dev/null +++ b/wwwroot/api/human_list.dspy @@ -0,0 +1,12 @@ +tenant_id = (await get_userorgid()) or '0' +status = params_kw.get('status', None) + +try: + result = await human_task_list(tenant_id, None, None, status) + data = json.loads(result) + if data.get('success') and 'tasks' in data: + data['rows'] = data.pop('tasks') + return json.dumps(data, ensure_ascii=False, default=str) + 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 index 97c4eb2..1bca641 100644 --- a/wwwroot/api/task_list.dspy +++ b/wwwroot/api/task_list.dspy @@ -1,13 +1,18 @@ tenant_id = (await get_userorgid()) or '0' -pipeline_id = params_kw.get('pipeline_id', None) +pipeline_id = params_kw.get('pipeline_id', params_kw.get('filter_pipeline_id', None)) +state_filter = params_kw.get('state', params_kw.get('filter_state', None)) limit = int(params_kw.get('limit', 100)) try: result = await pipeline_list(tenant_id, pipeline_id, limit) - # DataViewer期望rows键 data = json.loads(result) if data.get('success') and 'tasks' in data: - data['rows'] = data.pop('tasks') + tasks = data['tasks'] + # Apply state filter if specified (pipeline_list doesn't support it natively) + if state_filter: + tasks = [t for t in tasks if t.get('state') == state_filter] + data['rows'] = tasks + data['total'] = len(tasks) return json.dumps(data, ensure_ascii=False, default=str) return result except Exception as e: diff --git a/wwwroot/api/task_restart.dspy b/wwwroot/api/task_restart.dspy new file mode 100644 index 0000000..b5476a7 --- /dev/null +++ b/wwwroot/api/task_restart.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_restart(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/index.ui b/wwwroot/index.ui index 98e08b8..77be82e 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -16,13 +16,98 @@ "options": {"url": "{{entire_url('task_submit.ui')}}"}, "mode": "replace" }] - } + }, + {"widgettype": "Filler"} ] }, { "widgettype": "VBox", "id": "pipeline_task_content", - "options": {"width": "100%", "flex": "1"} + "options": {"width": "100%", "css": "filler"}, + "subwidgets": [ + { + "widgettype": "Tabular", + "id": "task_table", + "options": { + "width": "100%", + "height": "100%", + "css": "card", + "title": "任务中心", + "data_url": "{{entire_url('api/task_list.dspy')}}", + "data_method": "GET", + "page_rows": 20, + "toolbar": { + "tools": [ + {"name": "refresh", "label": "刷新", "selected_row": false}, + {"name": "view", "label": "查看详情", "selected_row": true} + ] + }, + "search_form": { + "fields": [ + {"name": "pipeline_id", "label": "产线", "uitype": "str", "placeholder": "筛选产线ID"}, + {"name": "state", "label": "状态", "uitype": "code", "data": [ + {"value": "", "text": "全部"}, + {"value": "submitted", "text": "已提交"}, + {"value": "running", "text": "运行中"}, + {"value": "waiting", "text": "等待人工"}, + {"value": "completed", "text": "已完成"}, + {"value": "failed", "text": "失败"}, + {"value": "paused", "text": "已暂停"}, + {"value": "cancelled", "text": "已取消"} + ]} + ] + }, + "row_options": { + "browserfields": { + "exclouded": ["tenant_id", "owner_id", "params"], + "cwidths": {"id": 10, "title": 20, "pipeline_id": 10, "state": 8, "current_version": 6, "created_at": 11}, + "alters": { + "state": { + "uitype": "code", + "data": [ + {"value": "submitted", "text": "已提交"}, + {"value": "running", "text": "运行中"}, + {"value": "waiting", "text": "⚠等待人工"}, + {"value": "completed", "text": "已完成"}, + {"value": "failed", "text": "失败"}, + {"value": "paused", "text": "已暂停"}, + {"value": "cancelled", "text": "已取消"} + ] + } + } + }, + "fields": [ + {"name": "id", "type": "str", "length": 32, "cwidth": 10, "uitype": "str", "label": "任务ID"}, + {"name": "title", "type": "str", "length": 255, "cwidth": 20, "uitype": "str", "label": "标题"}, + {"name": "pipeline_id", "type": "str", "length": 32, "cwidth": 10, "uitype": "str", "label": "产线"}, + {"name": "state", "type": "str", "length": 20, "cwidth": 8, "uitype": "code", "label": "状态"}, + {"name": "current_version", "type": "int", "cwidth": 6, "uitype": "int", "label": "版本"}, + {"name": "created_at", "type": "datetime", "cwidth": 11, "uitype": "str", "label": "创建时间"} + ] + } + }, + "binds": [ + { + "wid": "self", + "event": "refresh", + "actiontype": "method", + "target": "self", + "method": "render" + }, + { + "wid": "self", + "event": "view", + "actiontype": "urlwidget", + "target": "app.pipeline_task_content", + "mode": "replace", + "options": { + "url": "{{entire_url('task_detail.ui')}}", + "params": {"task_id": "${id}$"} + } + } + ] + } + ] } ] } diff --git a/wwwroot/step_panel.ui b/wwwroot/step_panel.ui new file mode 100644 index 0000000..c7c4ff6 --- /dev/null +++ b/wwwroot/step_panel.ui @@ -0,0 +1,117 @@ +{% set node_data = get_step_node_data(request) %} +{% set task_id = params_kw.get('task_id', '') %} +{% set step_name = params_kw.get('step_name', '') %} +{% set mode = params_kw.get('mode', 'view') %} + +{ + "widgettype": "VBox", + "options": {"width": "100%", "padding": "16px"}, + "subwidgets": [ + { + "widgettype": "HBox", + "options": {"width": "100%", "marginBottom": "16px", "gap": "12px", "alignItems": "center"}, + "subwidgets": [ + { + "widgettype": "Button", + "options": {"label": "← 返回任务详情"}, + "binds": [{ + "wid": "self", "event": "click", "actiontype": "urlwidget", + "target": "app.pipeline_task_content", + "options": {"url": "{{entire_url('task_detail.ui')}}?task_id={{task_id}}"}, + "mode": "replace" + }] + }, + {"widgettype": "Title", "options": {"text": "步骤产物: {{step_name}}", "cfontsize": 18}} + ] + }, + {% if node_data.success %} + { + "widgettype": "VBox", + "options": {"width": "100%"}, + "subwidgets": [ + {% if node_data.input %} + { + "widgettype": "VBox", + "options": {"width": "100%", "bgcolor": "var(--card-bg)", "padding": "16px", "marginBottom": "12px"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "📥 输入产物", "cfontsize": 15}}, + {"widgettype": "Text", "options": {"text": "{{json.dumps(node_data.input, ensure_ascii=False)}}"}} + ] + }, + {% endif %} + {% if node_data.output %} + { + "widgettype": "VBox", + "options": {"width": "100%", "bgcolor": "var(--card-bg)", "padding": "16px", "marginBottom": "12px"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "📤 输出产物", "cfontsize": 15}}, + {"widgettype": "Text", "options": {"text": "{{json.dumps(node_data.output, ensure_ascii=False)}}"}} + ] + }, + {% endif %} + {% if not node_data.input and not node_data.output %} + {"widgettype": "Text", "options": {"text": "该步骤暂无产物数据"}}, + {% endif %} + { + "widgettype": "Filler" + } + ] + }, + {% else %} + { + "widgettype": "Text", + "options": {"text": "加载失败: {{node_data.message or '未知错误'}}"} + }, + {% endif %} + + {% if mode == 'edit' %} + { + "widgettype": "VBox", + "options": {"width": "100%", "bgcolor": "var(--card-bg)", "padding": "16px", "marginTop": "16px"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "修改产物并级联重跑", "cfontsize": 15}}, + { + "widgettype": "Form", + "options": { + "name": "modify_form", + "submit_url": "{{entire_url('api/task_modify.dspy')}}", + "fields": [ + {"name": "task_id", "uitype": "hide", "value": "{{task_id}}"}, + {"name": "step_name", "uitype": "hide", "value": "{{step_name}}"}, + {"name": "rerun_from", "uitype": "hide", "value": "node"}, + {"name": "content", "uitype": "text", "label": "修改后的JSON产物", "placeholder": "留空则直接重跑此步骤及下游"} + ] + }, + "binds": [ + { + "wid": "self", "event": "submited", + "actiontype": "script", + "target": "self", + "script": "var tid='{{task_id}}';alert('修改成功,产线继续执行');location.href='{{entire_url('task_detail.ui')}}?task_id='+tid" + } + ] + } + ] + }, + {% else %} + { + "widgettype": "HBox", + "options": {"width": "100%", "marginTop": "16px", "gap": "8px"}, + "subwidgets": [ + { + "widgettype": "Button", + "options": {"label": "修改产物"}, + "binds": [{ + "wid": "self", "event": "click", "actiontype": "urlwidget", + "target": "app.pipeline_task_content", + "options": {"url": "{{entire_url('step_panel.ui')}}?task_id={{task_id}}&step_name={{step_name}}&mode=edit"}, + "mode": "replace" + }] + }, + {"widgettype": "Filler"} + ] + }, + {% endif %} + {"widgettype": "Filler"} + ] +} diff --git a/wwwroot/task_detail.ui b/wwwroot/task_detail.ui index 42611da..1ee7e75 100644 --- a/wwwroot/task_detail.ui +++ b/wwwroot/task_detail.ui @@ -1,73 +1,250 @@ +{% set ns = namespace() %} +{% set task_data = get_task_data(request) %} +{% if not task_data.success %} +{ + "widgettype": "VBox", + "options": {"width": "100%", "padding": "16px"}, + "subwidgets": [ + { + "widgettype": "Button", + "options": {"label": "返回列表"}, + "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": "{{task_data.message or '加载失败'}}"}} + ] +} +{% else %} +{% set task = task_data.task %} +{% set steps = task.steps or [] %} +{% set state_labels = {'submitted':'已提交','running':'运行中','completed':'已完成','failed':'失败','paused':'已暂停','cancelled':'已取消','waiting':'等待人工'} %} +{% set step_state_labels = {'pending':'待执行','running':'执行中','completed':'已完成','failed':'失败','skipped':'已跳过','waiting':'等待人工','rejected':'已驳回'} %} +{% set is_done = task.state in ('completed', 'failed', 'cancelled') %} { "widgettype": "VBox", "options": {"width": "100%", "padding": "16px"}, "subwidgets": [ { "widgettype": "HBox", - "options": {"width": "100%", "marginBottom": "16px", "gap": "12px", "alignItems": "center"}, + "options": {"width": "100%", "marginBottom": "12px", "gap": "12px", "alignItems": "center"}, "subwidgets": [ { "widgettype": "Button", - "options": {"label": "返回列表", "icon": "arrow-left"}, + "options": {"label": "返回列表"}, "binds": [{ "wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.pipeline_task_content", - "options": {"url": "{{entire_url('task_list.ui')}}"}, + "options": {"url": "{{entire_url('index.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": "Title", "options": {"text": "任务详情 - {{json.dumps(task.title or '', ensure_ascii=False)}}", "cfontsize": 18}}, + {"widgettype": "Text", "options": {"text": "状态: {{state_labels.get(task.state, task.state)}} | 版本: v{{task.current_version or 1}}"}} ] }, { "widgettype": "HBox", - "options": {"width": "100%", "flex": "1", "gap": "16px"}, + "options": {"width": "100%", "marginBottom": "12px", "gap": "8px"}, + "subwidgets": [ + {% if task.state == 'running' %} + { + "widgettype": "Button", + "options": {"label": "暂停", "css": "warning"}, + "binds": [{ + "wid": "self", "event": "click", "actiontype": "script", + "target": "self", + "script": "var u='{{entire_url('api/task_control.dspy')}}';var fd=new URLSearchParams();fd.append('task_id','{{task.id}}');fd.append('action','pause');fetch(u,{method:'POST',body:fd,credentials:'include'}).then(function(r){return r.json()}).then(function(d){if(d.success){location.reload()}else{alert(d.message)}})" + }] + }, + {% endif %} + {% if task.state == 'paused' %} + { + "widgettype": "Button", + "options": {"label": "恢复", "css": "primary"}, + "binds": [{ + "wid": "self", "event": "click", "actiontype": "script", + "target": "self", + "script": "var u='{{entire_url('api/task_control.dspy')}}';var fd=new URLSearchParams();fd.append('task_id','{{task.id}}');fd.append('action','resume');fetch(u,{method:'POST',body:fd,credentials:'include'}).then(function(r){return r.json()}).then(function(d){if(d.success){location.reload()}else{alert(d.message)}})" + }] + }, + {% endif %} + {% if not is_done %} + { + "widgettype": "Button", + "options": {"label": "取消", "css": "danger"}, + "binds": [{ + "wid": "self", "event": "click", "actiontype": "script", + "target": "self", + "script": "if(!confirm('确定取消任务?'))return;var u='{{entire_url('api/task_control.dspy')}}';var fd=new URLSearchParams();fd.append('task_id','{{task.id}}');fd.append('action','cancel');fetch(u,{method:'POST',body:fd,credentials:'include'}).then(function(r){return r.json()}).then(function(d){if(d.success){location.reload()}else{alert(d.message)}})" + }] + }, + {% endif %} + {% if is_done %} + { + "widgettype": "Button", + "options": {"label": "重新开始", "css": "primary"}, + "binds": [{ + "wid": "self", "event": "click", "actiontype": "script", + "target": "self", + "script": "if(!confirm('确定重新开始?'))return;var u='{{entire_url('api/task_restart.dspy')}}';var fd=new URLSearchParams();fd.append('task_id','{{task.id}}');fetch(u,{method:'POST',body:fd,credentials:'include'}).then(function(r){return r.json()}).then(function(d){if(d.success){location.reload()}else{alert(d.message)}})" + }] + } + {% endif %} + ] + }, + { + "widgettype": "Text", + "options": {"text": "步骤列表 ({{steps|length}} 个步骤)", "cfontsize": 14} + }, + {% for step in steps %} + { + "widgettype": "VBox", + "options": {"width": "100%", "bgcolor": "var(--card-bg)", "padding": "12px", "marginBottom": "8px"}, "subwidgets": [ { - "widgettype": "VBox", - "id": "detail_steps_area", - "options": {"width": "50%", "minHeight": "400px", "bgcolor": "var(--card-bg)", "padding": "16px"}, + "widgettype": "HBox", + "options": {"width": "100%", "justifyContent": "space-between", "alignItems": "center", "marginBottom": "8px"}, "subwidgets": [ - {"widgettype": "Text", "options": {"text": "步骤列表加载中..."}} + { + "widgettype": "Text", + "options": {"text": "{{json.dumps(step.display_name or step.step_name, ensure_ascii=False)}}{% if step.state == 'waiting' %} ⚠{% endif %}", "cfontsize": 14} + }, + { + "widgettype": "Text", + "options": {"text": "{{step_state_labels.get(step.state, step.state)}}", "color": "{% if step.state == 'completed' %}#4caf50{% elif step.state == 'waiting' %}#ff9800{% elif step.state == 'failed' %}#f44336{% else %}#888{% endif %}"} + } ] }, { - "widgettype": "VBox", - "id": "detail_node_area", - "options": {"width": "50%", "minHeight": "400px", "bgcolor": "var(--card-bg)", "padding": "16px"}, + "widgettype": "Text", + "options": {"text": "类型: {{step.step_type or '-'}} | 依赖: {{json.dumps(step.deps if step.deps is string else (step.deps or [])|join(',') or '无', ensure_ascii=False)}}"} + }, + {% if step.state == 'waiting' and step.human_task %} + { + "widgettype": "HBox", + "options": {"width": "100%", "marginTop": "8px", "gap": "8px", "alignItems": "flex-start"}, "subwidgets": [ - {"widgettype": "Text", "options": {"text": "点击左侧步骤查看产物"}} + { + "widgettype": "VBox", + "options": {"width": "50%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "⚠ 人工任务: {{step.human_task.task_type or '-'}}", "color": "#ff9800"}}, + {"widgettype": "Text", "options": {"text": "处理状态: {{step.human_task.status or '-'}}"}}, + {% if step.human_task.assignee_role %} + {"widgettype": "Text", "options": {"text": "处理角色: {{step.human_task.assignee_role}}"}}, + {% endif %} + {% if step.human_task.form_schema %} + {"widgettype": "Text", "options": {"text": "表单: {{json.dumps(step.human_task.form_schema, ensure_ascii=False)}}"}}, + {% endif %} + { + "widgettype": "Button", + "options": {"label": "查看产物", "css": "text"}, + "binds": [{ + "wid": "self", "event": "click", "actiontype": "urlwidget", + "target": "app.pipeline_task_content", + "options": {"url": "{{entire_url('step_panel.ui')}}?task_id={{task.id}}&step_name={{step.step_name}}"}, + "mode": "replace" + }] + } + ] + }, + {% if step.human_task.task_type == 'human_task' and step.human_task.status == 'pending' %} + { + "widgettype": "Form", + "options": { + "width": "50%", + "name": "human_form_{{loop.index}}", + "submit_url": "{{entire_url('api/human_complete.dspy')}}", + "fields": [ + {"name": "task_id", "uitype": "hide", "value": "{{task.id}}"}, + {"name": "step_name", "uitype": "hide", "value": "{{step.step_name}}"}, + {"name": "result", "uitype": "text", "label": "处理结果 (JSON)", "required": true, "placeholder": "{\"status\":\"ok\",\"note\":\"已完成\"}"} + ] + }, + "binds": [{ + "wid": "self", "event": "submited", + "actiontype": "script", + "target": "self", + "script": "location.reload()" + }] + }, + {% elif step.human_task.task_type == 'approval_gate' and step.human_task.status == 'pending' %} + { + "widgettype": "VBox", + "options": {"width": "50%", "gap": "8px"}, + "subwidgets": [ + { + "widgettype": "Button", + "options": {"label": "✓ 通过", "css": "primary"}, + "binds": [{ + "wid": "self", "event": "click", "actiontype": "script", + "target": "self", + "script": "var u='{{entire_url('api/approval_approve.dspy')}}';var fd=new URLSearchParams();fd.append('task_id','{{task.id}}');fd.append('step_name','{{step.step_name}}');fetch(u,{method:'POST',body:fd,credentials:'include'}).then(function(r){return r.json()}).then(function(d){if(d.success){location.reload()}else{alert(d.message)}})" + }] + }, + { + "widgettype": "Button", + "options": {"label": "✗ 驳回", "css": "danger"}, + "binds": [{ + "wid": "self", "event": "click", "actiontype": "script", + "target": "self", + "script": "var reason=prompt('驳回理由:');if(!reason)return;var u='{{entire_url('api/approval_reject.dspy')}}';var fd=new URLSearchParams();fd.append('task_id','{{task.id}}');fd.append('step_name','{{step.step_name}}');fd.append('comments',reason);fetch(u,{method:'POST',body:fd,credentials:'include'}).then(function(r){return r.json()}).then(function(d){if(d.success){location.reload()}else{alert(d.message)}})" + }] + } + ] + }, + {% endif %} + {"widgettype": "Filler"} ] - } + }, + {% elif step.state in ('completed', 'failed') %} + { + "widgettype": "HBox", + "options": {"width": "100%", "marginTop": "8px", "gap": "8px"}, + "subwidgets": [ + { + "widgettype": "Button", + "options": {"label": "查看产物", "css": "text"}, + "binds": [{ + "wid": "self", "event": "click", "actiontype": "urlwidget", + "target": "app.pipeline_task_content", + "options": {"url": "{{entire_url('step_panel.ui')}}?task_id={{task.id}}&step_name={{step.step_name}}"}, + "mode": "replace" + }] + }, + { + "widgettype": "Button", + "options": {"label": "修改并重跑", "css": "warning"}, + "binds": [{ + "wid": "self", "event": "click", "actiontype": "urlwidget", + "target": "app.pipeline_task_content", + "options": {"url": "{{entire_url('step_panel.ui')}}?task_id={{task.id}}&step_name={{step.step_name}}&mode=edit"}, + "mode": "replace" + }] + } + ] + }, + {% else %} + { + "widgettype": "Button", + "options": {"label": "查看详情", "css": "text"}, + "binds": [{ + "wid": "self", "event": "click", "actiontype": "urlwidget", + "target": "app.pipeline_task_content", + "options": {"url": "{{entire_url('step_panel.ui')}}?task_id={{task.id}}&step_name={{step.step_name}}"}, + "mode": "replace" + }] + }, + {% endif %} + {"widgettype": "Filler"} ] - } + }{% if not loop.last %},{% endif %} + {% endfor %} ] } +{% endif %} diff --git a/wwwroot/task_list.ui b/wwwroot/task_list.ui index 153a20c..47b8f0d 100644 --- a/wwwroot/task_list.ui +++ b/wwwroot/task_list.ui @@ -1,17 +1,82 @@ { - "widgettype": "DataViewer", + "widgettype": "Tabular", + "id": "task_table", "options": { - "url": "{{entire_url('/pipeline_task/api/task_list.dspy')}}", + "width": "100%", + "height": "100%", + "css": "card", "title": "任务中心", - "pageSize": 20, - "new_data_url": "{{entire_url('/pipeline_task/task_submit.ui')}}", - "fields": [ - {"name": "id", "title": "任务ID", "width": "120px"}, - {"name": "title", "title": "标题", "width": "30%"}, - {"name": "pipeline_type", "title": "产线类型", "width": "100px"}, - {"name": "state", "title": "状态", "width": "100px"}, - {"name": "current_version", "title": "版本", "width": "80px"}, - {"name": "created_at", "title": "创建时间", "width": "160px"} - ] - } + "data_url": "{{entire_url('api/task_list.dspy')}}", + "data_method": "GET", + "page_rows": 20, + "toolbar": { + "tools": [ + {"name": "refresh", "label": "刷新", "selected_row": false}, + {"name": "view", "label": "查看详情", "selected_row": true} + ] + }, + "search_form": { + "fields": [ + {"name": "pipeline_id", "label": "产线", "uitype": "str", "placeholder": "筛选产线ID"}, + {"name": "state", "label": "状态", "uitype": "code", "data": [ + {"value": "", "text": "全部"}, + {"value": "submitted", "text": "已提交"}, + {"value": "running", "text": "运行中"}, + {"value": "waiting", "text": "等待人工"}, + {"value": "completed", "text": "已完成"}, + {"value": "failed", "text": "失败"}, + {"value": "paused", "text": "已暂停"}, + {"value": "cancelled", "text": "已取消"} + ]} + ] + }, + "row_options": { + "browserfields": { + "exclouded": ["tenant_id", "owner_id", "params"], + "cwidths": {"id": 10, "title": 20, "pipeline_id": 10, "state": 8, "current_version": 6, "created_at": 11}, + "alters": { + "state": { + "uitype": "code", + "data": [ + {"value": "submitted", "text": "已提交"}, + {"value": "running", "text": "运行中"}, + {"value": "waiting", "text": "⚠等待人工"}, + {"value": "completed", "text": "已完成"}, + {"value": "failed", "text": "失败"}, + {"value": "paused", "text": "已暂停"}, + {"value": "cancelled", "text": "已取消"} + ] + } + } + }, + "fields": [ + {"name": "id", "type": "str", "length": 32, "cwidth": 10, "uitype": "str", "label": "任务ID"}, + {"name": "title", "type": "str", "length": 255, "cwidth": 20, "uitype": "str", "label": "标题"}, + {"name": "pipeline_id", "type": "str", "length": 32, "cwidth": 10, "uitype": "str", "label": "产线"}, + {"name": "state", "type": "str", "length": 20, "cwidth": 8, "uitype": "code", "label": "状态"}, + {"name": "current_version", "type": "int", "cwidth": 6, "uitype": "int", "label": "版本"}, + {"name": "created_at", "type": "datetime", "cwidth": 11, "uitype": "str", "label": "创建时间"} + ] + } + }, + "binds": [ + { + "wid": "self", + "event": "refresh", + "actiontype": "method", + "target": "self", + "method": "render" + }, + { + "wid": "self", + "event": "view", + "actiontype": "urlwidget", + "target": "app.pipeline_task_content", + "mode": "replace", + "options": { + "url": "{{entire_url('task_detail.ui')}}", + "params": {"task_id": "${id}$"} + } + } + ] }