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:
yumoqing 2026-07-08 21:03:32 +08:00
parent 6f85b7a282
commit b556ab0cdc
12 changed files with 632 additions and 67 deletions

View File

@ -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

View File

@ -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():

View 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)

View 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)

View 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)

View 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)

View File

@ -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:

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_restart(tenant_id, task_id)
return result
except Exception as e:
return json.dumps({"success": False, "message": str(e)}, ensure_ascii=False)

View File

@ -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
View 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"}
]
}

View File

@ -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 %}

View File

@ -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}$"}
}
}
]
}