182 lines
8.5 KiB
Plaintext
182 lines
8.5 KiB
Plaintext
# 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,
|
||
}
|