# opp_mining_popup.dspy - 需求挖掘批次列表(「⛏ 需求挖掘」菜单入口,P2 三级下钻第一级) # 交互范式对齐 opp_reports_list.dspy:会话当前项目过滤(本项目批次 + 平台级批次), # 整行点击 → opp_mining_clusters.dspy 弹出类别排名(第二级)。 # 顶部两个启动按钮:全量挖掘(scope=top)/ 指定类型挖掘(scope=targeted,prompt 输入关键词)。 # 可见性规则源 = opp_mining_flow.mining_status(与工具通道同一实现,语义不漂移)。 import json as _json uid = await get_user() if not uid: return {"widgettype": "Message", "options": {"title": "未登录", "message": "请先登录"}} 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' # 会话当前项目(统一解析:session→全局兜底+产线隔离+悬空自愈) _cur_pid = '' 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 '' except Exception: _cur_pid = '' # 用户机构(org 隔离) _uorg = '' async with DBPools().sqlorContext(dbname) as sor: _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 "") _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 mining_status _ok, _res = await mining_status(sor, _ctx, batch_id="") await sor.sqlExe("COMMIT", {}) clusters_url = entire_url("/pipeline-opportunity/api/opp_mining_clusters.dspy") start_url = entire_url("/pipeline-opportunity/api/opp_mining_start.dspy") self_url = entire_url("/pipeline-opportunity/api/opp_mining_popup.dspy") _q = [] if session_id: _q.append("session_id=" + session_id) if pipeline_id: _q.append("pipeline_id=" + pipeline_id) _qs = ("?" + "&".join(_q)) if _q else "" _STATUS_STYLE = { "done": ("已完成", "#16a34a", "#f0fdf4"), "failed": ("失败", "#dc2626", "#fef2f2"), "pulling": ("拉取中", "#d97706", "#fffbeb"), "embedding": ("向量化中", "#d97706", "#fffbeb"), "clustering": ("聚类中", "#d97706", "#fffbeb"), "naming": ("命名中", "#d97706", "#fffbeb"), } def _refresh_script(): return ("var r=await fetch(" + _json.dumps(self_url + _qs) + ");" "var d=await r.json();if(d){bricks.widgetBuild(d,bricks.app);}") def _start_script(scope): # targeted 先 prompt 关键词;启动后轮询刷新列表 kw_js = ("var kw=prompt('输入要挖掘的需求类型关键词(如:合同管理)');if(!kw){return;}kw='&keyword='+encodeURIComponent(kw);") if scope == "targeted" else "var kw='';" base = start_url + _qs + ("&" if _q else "?") + "scope=" + scope return (kw_js + "var r=await fetch(" + _json.dumps(base) + "+kw);" "var d=await r.json();" "if(!d||d.status!=='ok'){alert((d&&d.message)||'启动失败');return;}" "alert('挖掘批次已启动:'+d.batch_id+'(后台执行,列表将自动刷新)');" + _refresh_script()) rows = [] # ── 顶部:启动按钮 + 说明 ── rows.append({"widgettype": "Text", "options": { "text": "需求挖掘:把众包平台(猪八戒/一品威客/Freelancer/PPH)的软件需求做语义聚类," "发现市场热门软件类别(TopX)或分析你指定的需求类型。批次后台执行,完成后点击行查看类别排名。", "cfontsize": 0.8, "color": "#64748b", "halign": "left", "wrap": True, "margin": "0 0 8px 0"}}) rows.append({"widgettype": "HBox", "options": {"width": "100%", "gap": "10px", "margin": "0 0 12px 0"}, "subwidgets": [ {"widgettype": "Button", "options": {"label": "⛏ 启动全量挖掘(TopX 热门类别)", "css": "primary"}, "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": _start_script("top")}]}, {"widgettype": "Button", "options": {"label": "🎯 指定类型挖掘", "css": "small"}, "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": _start_script("targeted")}]}, {"widgettype": "Filler"}, {"widgettype": "Button", "options": {"label": "🔄 刷新", "css": "small"}, "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": _refresh_script()}]}, ]}) if not _ok: rows.append({"widgettype": "Text", "options": { "text": "查询失败:" + str(_res)[:200], "color": "#dc2626", "padding": "12px", "halign": "left"}}) else: batches = _res if isinstance(_res, list) else [] if not batches: rows.append({"widgettype": "Text", "options": { "text": "还没有挖掘批次。点击上方「⛏ 启动全量挖掘」开始(首次全量约3-5分钟;" "embedding 有全机构共享缓存,二次批次显著更快)。", "color": "#64748b", "padding": "12px", "halign": "left", "wrap": True}}) for b in batches: bid = str(b.get("id") or "") status = str(b.get("status") or "") s_label, s_color, s_bg = _STATUS_STYLE.get(status, (status, "#64748b", "#f8fafc")) scope = str(b.get("scope") or "") scope_txt = "全量 TopX" if scope == "top" else "指定类型" try: _params = _json.loads(b.get("params_json") or "{}") except Exception: _params = {} if scope == "targeted": kw = _params.get("keyword") or _params.get("category") or "" if kw: scope_txt += ":%s" % kw try: _stats = _json.loads(b.get("stats_json") or "{}") except Exception: _stats = {} meta_bits = [] if _stats.get("demands"): meta_bits.append("需求 %s 条" % _stats["demands"]) if _stats.get("clusters"): meta_bits.append("类别 %s 个" % _stats["clusters"]) if _stats.get("elapsed_s"): meta_bits.append("耗时 %ss" % _stats["elapsed_s"]) if not str(b.get("project_id") or ""): meta_bits.append("平台级") if status == "failed" and b.get("error_msg"): meta_bits.append("原因:" + str(b.get("error_msg"))[:80]) meta = " | ".join(meta_bits) if meta_bits else str(b.get("created_at") or "")[:16] # done 批次整行可点下钻;进行中/失败批次不可点(无结果可看) if status == "done": qstr = ("&" + "&".join(_q)) if _q else "" script = ("var old=bricks.getWidgetById('opp_mine_clusters_pw',bricks.app);" "if(old&&old.destroy){old.destroy();}" "var r=await fetch(" + _json.dumps(clusters_url) + "+'?batch_id='+encodeURIComponent(" + _json.dumps(bid) + ")+" + _json.dumps(qstr) + ");" "var d=await r.json();if(d){bricks.widgetBuild(d,bricks.app);}") row_opts = {"width": "100%", "gap": "10px", "alignItems": "center", "padding": "10px 14px", "border": "1px solid #e2e8f0", "borderRadius": "8px", "margin": "0 0 8px 0", "bgcolor": "#ffffff", "cursor": "pointer"} row_binds = [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": script}] arrow = {"widgettype": "Text", "options": {"text": "›", "cfontsize": 1.1, "color": "#94a3b8"}} else: row_opts = {"width": "100%", "gap": "10px", "alignItems": "center", "padding": "10px 14px", "border": "1px solid #e2e8f0", "borderRadius": "8px", "margin": "0 0 8px 0", "bgcolor": "#f8fafc"} row_binds = [] arrow = None subs = [ {"widgettype": "Text", "options": {"text": scope_txt, "cfontsize": 1.0, "color": "#0f172a", "halign": "left"}}, {"widgettype": "Text", "options": {"text": s_label, "cfontsize": 0.75, "color": s_color, "bgcolor": s_bg, "padding": "2px 8px", "borderRadius": "10px"}}, {"widgettype": "Filler"}, {"widgettype": "Text", "options": {"text": meta, "cfontsize": 0.75, "color": "#94a3b8", "halign": "right", "wrap": True}}, ] if arrow: subs.append(arrow) row = {"widgettype": "HBox", "options": row_opts, "subwidgets": subs} if row_binds: row["binds"] = row_binds rows.append(row) return { "widgettype": "VBox", "options": {"css": "filler", "width": "100%", "height": "100%", "padding": "12px 16px", "gap": "6px", "overflow": "auto"}, "subwidgets": rows, }