# opp_mining_clusters.dspy - 挖掘批次的类别排名(三级下钻第二级) # 入参:batch_id。可见性规则源 = opp_mining_flow.list_clusters(含批次项目/org 校验)。 # 整行点击 → opp_mining_items.dspy 弹出类内需求明细(第三级,每条带来源URL)。 # 弹窗容器 id=opp_mine_clusters_pw(与列表页下钻脚本约定一致)。 import json as _json uid = await get_user() if not uid: return {"widgettype": "Message", "options": {"title": "未登录", "message": "请先登录"}} batch_id = ((params_kw or {}).get("batch_id") or "").strip() if not batch_id: return {"widgettype": "Message", "options": {"title": "缺少参数", "message": "缺少 batch_id"}} dbname = get_module_dbname('pipeline-opportunity') session_id = (params_kw or {}).get('session_id', '') or '' pipeline_id = (params_kw or {}).get('pipeline_id', '') or 'opportunity_general' _cur_pid = '' _uorg = '' try: from pipeline_service.workspace import get_session_project_id async with DBPools().sqlorContext(dbname) as sor: _cur_pid = await get_session_project_id(sor, uid, session_id, pipeline_id) or '' _urecs = await sor.sqlExe("SELECT orgid FROM users WHERE id=${u}$", {"u": uid}) await sor.sqlExe("COMMIT", {}) if _urecs: _uorg = str(getattr(_urecs[0], "orgid", "") or "") except Exception: pass _ctx = {"project_id": _cur_pid, "user_id": uid, "org_id": _uorg, "pipeline_id": pipeline_id, "session_id": session_id} async with DBPools().sqlorContext(dbname) as sor: from pipeline_opportunity.opp_mining_flow import list_clusters _ok, _res = await list_clusters(sor, _ctx, batch_id) await sor.sqlExe("COMMIT", {}) items_url = entire_url("/pipeline-opportunity/api/opp_mining_items.dspy") rows = [] if not _ok: rows.append({"widgettype": "Text", "options": { "text": str(_res)[:200], "color": "#dc2626", "padding": "12px", "halign": "left"}}) else: clusters = _res if isinstance(_res, list) else [] if not clusters: rows.append({"widgettype": "Text", "options": { "text": "该批次没有聚类结果(可能范围内无需求数据)", "color": "#64748b", "padding": "12px", "halign": "left"}}) for c in clusters: cid = str(c.get("id") or "") name = str(c.get("name") or "未命名") cnt = int(c.get("doc_count") or 0) share = float(c.get("share") or 0) * 100 rank = int(c.get("heat_rank") or 0) try: ev = _json.loads(c.get("naming_evidence") or "{}") except Exception: ev = {} samples = ev.get("samples") or [] ev_txt = ("命名依据:" + " / ".join(str(s)[:24] for s in samples[:3])) if samples else "" by_txt = "(规则命名)" if ev.get("by") == "rule" else "" q_extra = [] if session_id: q_extra.append("session_id=" + session_id) if pipeline_id: q_extra.append("pipeline_id=" + pipeline_id) qstr = ("&" + "&".join(q_extra)) if q_extra else "" script = ("var old=bricks.getWidgetById('opp_mine_items_pw',bricks.app);" "if(old&&old.destroy){old.destroy();}" "var r=await fetch(" + _json.dumps(items_url) + "+'?cluster_id='+encodeURIComponent(" + _json.dumps(cid) + ")+" + _json.dumps(qstr) + ");" "var d=await r.json();if(d){bricks.widgetBuild(d,bricks.app);}") rows.append({ "widgettype": "HBox", "options": {"width": "100%", "gap": "10px", "alignItems": "center", "padding": "10px 14px", "border": "1px solid #e2e8f0", "borderRadius": "8px", "margin": "0 0 8px 0", "bgcolor": "#ffffff", "cursor": "pointer"}, "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": script}], "subwidgets": [ {"widgettype": "Text", "options": {"text": "#%d" % rank, "cfontsize": 0.85, "color": "#94a3b8"}}, {"widgettype": "VBox", "options": {"gap": "2px"}, "subwidgets": [ {"widgettype": "Text", "options": {"text": name + by_txt, "cfontsize": 1.0, "color": "#0f172a", "halign": "left"}}, {"widgettype": "Text", "options": {"text": ev_txt, "cfontsize": 0.72, "color": "#94a3b8", "halign": "left", "wrap": True}}, ]}, {"widgettype": "Filler"}, {"widgettype": "Text", "options": {"text": "%d 条 | %.1f%%" % (cnt, share), "cfontsize": 0.85, "color": "#64748b"}}, {"widgettype": "Text", "options": {"text": "›", "cfontsize": 1.1, "color": "#94a3b8"}}, ]}) return { "id": "opp_mine_clusters_pw", "widgettype": "PopupWindow", "options": {"title": "类别排名(批次 %s)" % batch_id[:8], "width": "640px", "height": "70%", "auto_open": True, "resizable": True}, "subwidgets": [{ "widgettype": "VBox", "options": {"width": "100%", "height": "100%", "padding": "12px 16px", "gap": "4px", "overflow": "auto"}, "subwidgets": [ {"widgettype": "Text", "options": { "text": "点击任一类别,查看类内需求明细(每条带来源链接可查证)。", "cfontsize": 0.78, "color": "#64748b", "halign": "left", "margin": "0 0 8px 0"}}, ] + rows, }], }