# my_todos_popup.dspy - 我的待办列表(返回 bricks widget 描述,由 todo_badge.js 构建) # 列表只做导航:每条待办点「查看内容并处理」→ todo_detail.dspy 渲染正文后再确认。 import json as _json user_id = await get_user() if not user_id: return {"widgettype": "Message", "options": {"title": "未登录", "message": "请先登录后查看待办"}} todos = await list_my_human_todos(user_id) dbname = get_module_dbname('pipeline-sdlc') TYPE_LABEL = { 'requirement_confirmation': '需求确认', 'design_confirmation': '设计确认', 'bug_acceptance': 'Bug 验收', 'flow_plan_confirm': '流程裁剪确认', 'tech_template_confirm': '技术方案骨架确认', 'material_supply': '实体材料缺口', } TYPE_COLOR = { 'requirement_confirmation': '#2563eb', 'design_confirmation': '#2563eb', 'bug_acceptance': '#f0a040', 'flow_plan_confirm': '#7c3aed', 'tech_template_confirm': '#0891b2', 'material_supply': '#d97706', } detail_url = entire_url("/pipeline-sdlc/api/todo_detail.dspy") ticket_detail_url = entire_url("/ticket/api/ticket_detail_popup.dspy") wallet_detail_url = entire_url("/pipeline-llm/api/llm_wallet_todo_popup.dspy") def _s(v): if v is None: return '' return str(v) def _clip(s, n): s = ' '.join(_s(s).split()) if len(s) > n: return s[:n] + '…' return s # 项目名(一次查全,避免逐条查库) proj_ids = [] for t in (todos or []): pid = _s(t.get('project_id')) if pid and pid not in proj_ids: proj_ids.append(pid) proj_names = {} if proj_ids: id_list = ','.join(["'" + p.replace("'", "") + "'" for p in proj_ids]) async with DBPools().sqlorContext(dbname) as sor: precs = await sor.sqlExe("SELECT id, name FROM sd_projects WHERE id IN (" + id_list + ")", {}) await sor.sqlExe("COMMIT", {}) for r in (precs or []): proj_names[_s(getattr(r, 'id', ''))] = _s(getattr(r, 'name', '')) cards = [] for t in (todos or []): src = _s(t.get('source')) tid = _s(t.get('id')) kind = 'question' if src == 'question' else 'human_task' task_type = _s(t.get('task_type')) if src == 'ticket': # 工单待办(2026-09-10 接入平台待办):badge 用 provider 给的分类 badge = _s(t.get('badge')) or '工单' badge_color = '#0ea5e9' title = _clip(t.get('title'), 80) or '工单' summary = _clip(t.get('description'), 110) elif src == 'llm_wallet': # 模型账号钱包余额不足(2026-09-11 接入平台待办):状态派生,充值到账自动消失 badge = _s(t.get('badge')) or '钱包充值' badge_color = '#dc2626' title = _clip(t.get('title'), 80) or '钱包充值' summary = _clip(t.get('description'), 110) elif src == 'question': badge = '待答问题' badge_color = '#8b5cf6' title = _clip(t.get('question'), 80) or '冒泡问题' summary = '提问角色:' + (_s(t.get('from_role')) or '-') + ' | 类型:' + (_s(t.get('problem_type')) or '-') else: badge = TYPE_LABEL.get(task_type, '人类任务') badge_color = TYPE_COLOR.get(task_type, '#2563eb') title = _clip(t.get('title'), 80) or '待办任务' summary = _clip(t.get('description'), 110) pid = _s(t.get('project_id')) meta_bits = [] pname = proj_names.get(pid) or _s(t.get('project_name')) or ('(项目已删除)' if pid else '') if pname: meta_bits.append('项目:' + pname) if t.get('created_at'): meta_bits.append(_s(t.get('created_at'))) if src == 'ticket': # 工单待办详情走 ticket 模块自持端点(from_todo=1 → 弹窗 id 用待办专用, # 关闭时连带关待办列表弹窗) open_script = ("var du=" + _json.dumps(ticket_detail_url) + "+'?from_todo=1&id='+encodeURIComponent(" + _json.dumps(tid) + ");" "var rp=await fetch(du);var dd=await rp.json();" "await bricks.widgetBuild(dd,bricks.app);") elif src == 'llm_wallet': # 钱包待办详情走 pipeline-llm 自持端点(弹窗内直接充值) open_script = ("var du=" + _json.dumps(wallet_detail_url) + "+'?id='+encodeURIComponent(" + _json.dumps(tid) + ");" "var rp=await fetch(du);var dd=await rp.json();" "await bricks.widgetBuild(dd,bricks.app);") else: open_script = ("var du=" + _json.dumps(detail_url) + "+'?kind=" + kind + "&id='+encodeURIComponent(" + _json.dumps(tid) + ");" "var rp=await fetch(du);var dd=await rp.json();" "await bricks.widgetBuild(dd,bricks.app);") sub = [ { "widgettype": "HBox", "options": {"width": "100%", "gap": "8px", "alignItems": "center"}, "subwidgets": [ {"widgettype": "Text", "options": {"text": badge, "cfontsize": 0.7, "color": "#ffffff", "padding": "1px 8px", "bgcolor": badge_color, "borderRadius": "10px", "whiteSpace": "nowrap"}}, {"widgettype": "Text", "options": {"text": title, "cfontsize": 0.95, "color": "#0f172a"}} ] } ] if meta_bits: sub.append({"widgettype": "Text", "options": {"text": ' | '.join(meta_bits), "cfontsize": 0.7, "color": "#94a3b8"}}) if summary: sub.append({"widgettype": "Text", "options": {"text": summary, "cfontsize": 0.78, "color": "#475569", "width": "100%"}}) sub.append({ "widgettype": "HBox", "options": {"width": "100%", "gap": "6px", "halign": "right"}, "subwidgets": [{ "widgettype": "Button", "options": {"label": "查看内容并处理", "css": "primary small"}, "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": open_script}] }] }) cards.append({ "widgettype": "VBox", "options": {"width": "100%", "bgcolor": "#ffffff", "border": "1px solid #e2e8f0", "borderRadius": "8px", "padding": "10px 14px", "gap": "6px", "marginBottom": "6px"}, "subwidgets": sub }) if not cards: cards = [{"widgettype": "Text", "options": {"text": "暂无待办", "cfontsize": 0.9, "color": "#94a3b8", "padding": "16px"}}] # embed=1(手机端「待办」Tab 整页渲染):返回纯 VBox,不套 PopupWindow 弹窗壳 _embed = ((params_kw or {}).get('embed') or '').strip() if _embed == '1': return { "widgettype": "VBox", "options": {"css": "filler", "width": "100%", "height": "100%", "padding": "0", "gap": "0px"}, "subwidgets": [{ "widgettype": "VScrollPanel", "options": {"css": "filler", "width": "100%", "height": "100%", "padding": "12px", "gap": "8px"}, "subwidgets": cards }] } return { "widgettype": "PopupWindow", "id": "my_todos_pw", "options": {"title": "我的待办(" + str(len(todos or [])) + ")", "width": "76%", "height": "80%", "auto_open": True, "resizable": True}, "subwidgets": [{ "widgettype": "VScrollPanel", "options": {"css": "filler", "width": "100%", "height": "100%", "padding": "12px", "gap": "8px"}, "subwidgets": cards }] }