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
This commit is contained in:
parent
6f85b7a282
commit
b556ab0cdc
@ -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
|
||||
|
||||
@ -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():
|
||||
|
||||
14
wwwroot/api/approval_approve.dspy
Normal file
14
wwwroot/api/approval_approve.dspy
Normal file
@ -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)
|
||||
15
wwwroot/api/approval_reject.dspy
Normal file
15
wwwroot/api/approval_reject.dspy
Normal file
@ -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)
|
||||
23
wwwroot/api/human_complete.dspy
Normal file
23
wwwroot/api/human_complete.dspy
Normal file
@ -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)
|
||||
12
wwwroot/api/human_list.dspy
Normal file
12
wwwroot/api/human_list.dspy
Normal file
@ -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)
|
||||
@ -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:
|
||||
|
||||
11
wwwroot/api/task_restart.dspy
Normal file
11
wwwroot/api/task_restart.dspy
Normal 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_restart(tenant_id, task_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "message": str(e)}, ensure_ascii=False)
|
||||
@ -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}$"}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
117
wwwroot/step_panel.ui
Normal file
117
wwwroot/step_panel.ui
Normal file
@ -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"}
|
||||
]
|
||||
}
|
||||
@ -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 %}
|
||||
|
||||
@ -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}$"}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user