feat: /task 任务树弹窗(task_tree/task_io dspy + widget 透传)

This commit is contained in:
ymq 2026-08-18 11:57:01 +08:00
parent f024733781
commit eba0f17767
3 changed files with 238 additions and 1 deletions

View File

@ -130,7 +130,12 @@ if action == 'send_message':
result = data.get('result', '')
yield json.dumps({"content": result + "\n\n"}, ensure_ascii=False) + '\n'
elif t == 'reply':
yield json.dumps({"content": data.get('message', '')}, ensure_ascii=False) + '\n'
msg = data.get('message', '')
if isinstance(msg, dict) and msg.get('widgettype'):
# widget JSON顶层透传前端 AgentOut 检测 widgettype 渲染(如 /task 的 PopupWindow
yield json.dumps(msg, ensure_ascii=False) + '\n'
else:
yield json.dumps({"content": msg}, ensure_ascii=False) + '\n'
elif t == 'ask_user':
yield json.dumps({"content": "❓ " + data.get('message', '')}, ensure_ascii=False) + '\n'
elif t == 'error':

161
wwwroot/api/task_io.dspy Normal file
View File

@ -0,0 +1,161 @@
# task_io.dspy - 任务的输入输出ResourceBrowser 右侧面板5 秒自动刷新)
import json
uid = await get_user()
if not uid:
uid = 'user-01'
task_id = (params_kw or {}).get('id', '').strip()
dbname = get_module_dbname('pipeline-sdlc')
_state_colors = {
'completed': '#16a34a',
'approved': '#16a34a',
'running': '#2563eb',
'failed': '#dc2626',
'waiting': '#f0a040',
'submitted': '#64748b',
'review': '#2196f3',
'rejected': '#dc2626',
'paused': '#f0a040',
'cancelled': '#64748b',
}
if not task_id:
return json.dumps({
"widgettype": "Text",
"options": {"text": "请选择左侧任务查看输入输出", "cfontsize": 0.9, "color": "#94a3b8", "padding": "16px"},
}, ensure_ascii=False)
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.sqlExe(
"SELECT id, title, role, state, params, last_error, created_at, updated_at "
"FROM pipeline_tasks WHERE id=${t}$", {"t": task_id})
if not recs:
return json.dumps({
"widgettype": "Text",
"options": {"text": "任务不存在", "cfontsize": 0.9, "color": "#94a3b8", "padding": "16px"},
}, ensure_ascii=False)
task = recs[0]
title = getattr(task, 'title', '') or ''
role = getattr(task, 'role', '') or ''
state = getattr(task, 'state', '') or ''
last_error = getattr(task, 'last_error', '') or ''
created_at = str(getattr(task, 'created_at', '') or '')[:16]
updated_at = str(getattr(task, 'updated_at', '') or '')[:16]
# 输入params 里的 description / input_text
params_str = getattr(task, 'params', '') or ''
description = ''
try:
p = json.loads(params_str) if params_str else {}
description = p.get('description', '') or p.get('input_text', '') or p.get('prompt', '') or ''
except Exception:
description = params_str
# 输出:交付件
dels = await sor.sqlExe(
"SELECT deliverable_type, title, content, review_status, quality_score, review_comment "
"FROM pipeline_deliverables WHERE task_id=${t}$ ORDER BY created_at DESC", {"t": task_id})
sc = _state_colors.get(state, '#64748b')
subwidgets = []
# 标题行:任务名 + 状态
subwidgets.append({
"widgettype": "Text",
"options": {"text": title, "halign": "left", "cfontsize": 1.15, "style": {"fontWeight": "bold", "color": "#1e293b"}},
})
subwidgets.append({
"widgettype": "HBox",
"options": {"gap": "12px", "width": "100%"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "状态: " + state, "cfontsize": 0.8, "color": sc, "halign": "left"}},
{"widgettype": "Text", "options": {"text": "角色: " + role, "cfontsize": 0.8, "color": "#64748b", "halign": "left"}},
{"widgettype": "Text", "options": {"text": "更新: " + updated_at, "cfontsize": 0.75, "color": "#94a3b8", "halign": "left"}},
{"widgettype": "Filler"},
],
})
if last_error:
subwidgets.append({
"widgettype": "Text",
"options": {"text": "错误: " + last_error[:300], "halign": "left", "cfontsize": 0.8, "css": "resp-error", "wrap": True},
})
# 输入
subwidgets.append({
"widgettype": "Text",
"options": {"text": "—— 输入 ——", "halign": "left", "cfontsize": 0.85, "style": {"fontWeight": "bold", "color": "#3b82f6", "marginTop": "8px"}},
})
subwidgets.append({
"widgettype": "Text",
"options": {"text": (description or "(无)")[:4000], "halign": "left", "cfontsize": 0.85, "color": "#334155", "wrap": True, "css": "card"},
})
# 输出
subwidgets.append({
"widgettype": "Text",
"options": {"text": "—— 输出(交付件)——", "halign": "left", "cfontsize": 0.85, "style": {"fontWeight": "bold", "color": "#16a34a", "marginTop": "8px"}},
})
if not dels:
subwidgets.append({
"widgettype": "Text",
"options": {"text": "(暂无交付件)", "halign": "left", "cfontsize": 0.8, "color": "#94a3b8"},
})
else:
for d in dels:
d_type = getattr(d, 'deliverable_type', '') or ''
d_title = getattr(d, 'title', '') or ''
d_content = getattr(d, 'content', '') or ''
d_status = getattr(d, 'review_status', '') or ''
d_score = getattr(d, 'quality_score', '') or ''
d_comment = getattr(d, 'review_comment', '') or ''
card = {
"widgettype": "VBox",
"options": {"css": "card", "gap": "4px", "width": "100%", "padding": "8px"},
"subwidgets": [
{"widgettype": "HBox", "options": {"gap": "8px", "width": "100%"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": d_type, "cfontsize": 0.8, "color": "#3b82f6", "halign": "left"}},
{"widgettype": "Text", "options": {"text": d_title, "cfontsize": 0.8, "color": "#1e293b", "halign": "left"}},
{"widgettype": "Filler"},
{"widgettype": "Text", "options": {"text": d_status, "cfontsize": 0.75, "color": "#94a3b8", "halign": "left"}},
]},
{"widgettype": "Text", "options": {"text": (d_content or "")[:3000], "cfontsize": 0.78, "color": "#475569", "wrap": True, "halign": "left"}},
],
}
if d_comment:
card["subwidgets"].append({
"widgettype": "Text",
"options": {"text": "审核意见: " + d_comment[:300], "cfontsize": 0.75, "color": "#f0a040", "wrap": True, "halign": "left"},
})
subwidgets.append(card)
# 5 秒刷新on_parent 事件add_widget 时触发)里 setInterval重新 render_browser 刷新当前任务内容
refresh_script = (
"if(!window._task_io_timer){"
"window._task_io_timer=setInterval(function(){"
"var rb=bricks.getWidgetById('task_rb',bricks.app);"
"if(rb&&rb.current_id){var cid=rb.current_id;rb.current_id=undefined;rb.render_browser(cid);}"
"},5000);}"
)
resp = {
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "12px", "gap": "6px", "overflowY": "auto"},
"subwidgets": subwidgets,
"binds": [{
"wid": "self",
"event": "on_parent",
"actiontype": "script",
"target": "self",
"script": refresh_script,
}],
}
return json.dumps(resp, ensure_ascii=False)

View File

@ -0,0 +1,71 @@
# task_tree.dspy - 任务树(懒加载:无 id 返回根节点id=__root__ 返回任务列表)
import json
uid = await get_user()
if not uid:
uid = 'user-01'
node_id = (params_kw or {}).get('id', '').strip()
dbname = get_module_dbname('pipeline-sdlc')
_state_icons = {
'completed': '\u2705',
'approved': '\u2705',
'running': '\U0001f7e1',
'failed': '\u274c',
'waiting': '\u26a0\ufe0f',
'submitted': '\u2b1c',
'review': '\U0001f440',
'rejected': '\U0001f6ab',
'paused': '\u23f8\ufe0f',
'cancelled': '\U0001f6d1',
}
async with DBPools().sqlorContext(dbname) as sor:
# 当前项目
recs = await sor.sqlExe(
"SELECT current_project_id FROM pipeline_agent_settings WHERE user_id=${u}$",
{"u": uid})
pid = getattr(recs[0], 'current_project_id', '') if recs else ''
if not pid:
if not node_id:
return json.dumps([{"id": "__root__", "label": "请先选择项目", "is_leaf": True}], ensure_ascii=False)
return json.dumps([], ensure_ascii=False)
# 项目名
pname = ''
precs = await sor.sqlExe("SELECT name FROM sd_projects WHERE id=${p}$", {"p": pid})
if precs:
pname = getattr(precs[0], 'name', '') or ''
if not node_id:
# 根节点
return json.dumps([{"id": "__root__", "label": pname or "项目", "is_leaf": False}], ensure_ascii=False)
if node_id != '__root__':
return json.dumps([], ensure_ascii=False)
# 任务列表(子节点)
tasks = await sor.sqlExe(
"SELECT id, title, role, state FROM pipeline_tasks WHERE tenant_id=${p}$ ORDER BY created_at ASC",
{"p": pid})
nodes = []
for t in (tasks or []):
tid = getattr(t, 'id', '')
title = getattr(t, 'title', '') or ''
role = getattr(t, 'role', '') or ''
state = getattr(t, 'state', '') or ''
if not tid or not title:
continue
icon = _state_icons.get(state, '\u2b1c')
nodes.append({
"id": tid,
"label": "{} [{}] {}".format(icon, role, title),
"is_leaf": True,
})
return json.dumps(nodes, ensure_ascii=False)