feat(mining-ui): P2需求挖掘三级下钻UI——⛏需求挖掘菜单(无require_project,平台级批次也可见)→批次列表(启动全量/指定类型按钮+状态色标+done行下钻)→类别排名PopupWindow(排名/占比/命名依据)→类内明细(每条带来源,src_id复用opp_item_detail详情弹窗);opp_mining_start.dspy复用start_mining与工具通道同实现;cluster_detail补src_id字段;load_path注册4端点
This commit is contained in:
parent
3fadee97b9
commit
25348fa2e1
@ -550,6 +550,8 @@ def register_opp_ability():
|
||||
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
|
||||
{"label": "✅ 研发审批", "icon": "", "url": "/pipeline-opportunity/opp_approvals/index.ui",
|
||||
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
|
||||
{"label": "⛏ 需求挖掘", "icon": "", "url": "/pipeline-opportunity/api/opp_mining_popup.dspy",
|
||||
"type": "popup", "width": "76%", "height": "82%"},
|
||||
{"label": "📋 数据参考", "icon": "", "url": "/pipeline-opportunity/api/opp_references_popup.dspy",
|
||||
"type": "popup", "width": "70%", "height": "76%", "require_project": True},
|
||||
],
|
||||
|
||||
@ -280,7 +280,7 @@ async def cluster_detail(sor, ctx, cluster_id, limit=20):
|
||||
if not ok:
|
||||
return False, err
|
||||
items = rows_to_dicts(await sor.sqlExe(
|
||||
"SELECT id, title, source, budget_wan, url, publish_time FROM opp_demand_snap "
|
||||
"SELECT id, src_id, title, source, budget_wan, url, publish_time FROM opp_demand_snap "
|
||||
"WHERE cluster_id=${c}$ ORDER BY budget_wan DESC LIMIT ${n}$",
|
||||
{"c": cluster_id, "n": int(limit)}), limit=int(limit))
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
|
||||
@ -35,6 +35,10 @@ PATHS_LOGINED = [
|
||||
"/%s/api/opp_reports_list.dspy" % MOD,
|
||||
"/%s/api/opp_report_detail.dspy" % MOD,
|
||||
"/%s/api/opp_report_file_dl.dspy" % MOD,
|
||||
"/%s/api/opp_mining_popup.dspy" % MOD,
|
||||
"/%s/api/opp_mining_start.dspy" % MOD,
|
||||
"/%s/api/opp_mining_clusters.dspy" % MOD,
|
||||
"/%s/api/opp_mining_items.dspy" % MOD,
|
||||
]
|
||||
for t in TABLES:
|
||||
PATHS_LOGINED += [
|
||||
|
||||
116
wwwroot/api/opp_mining_clusters.dspy
Normal file
116
wwwroot/api/opp_mining_clusters.dspy
Normal file
@ -0,0 +1,116 @@
|
||||
# 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%", "modal": 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,
|
||||
}],
|
||||
}
|
||||
120
wwwroot/api/opp_mining_items.dspy
Normal file
120
wwwroot/api/opp_mining_items.dspy
Normal file
@ -0,0 +1,120 @@
|
||||
# opp_mining_items.dspy - 挖掘类别的类内需求明细(三级下钻第三级)
|
||||
# 入参:cluster_id。可见性经 opp_mining_flow.cluster_detail(含批次项目/org 校验)。
|
||||
# 每条带来源(source_text 说明文字),整行点击 → opp_item_detail.dspy(爬虫平台详情弹窗,
|
||||
# 快照 src_id 即爬虫平台 tenders.id,直接复用既有详情通道)。
|
||||
|
||||
import json as _json
|
||||
|
||||
uid = await get_user()
|
||||
if not uid:
|
||||
return {"widgettype": "Message", "options": {"title": "未登录", "message": "请先登录"}}
|
||||
|
||||
cluster_id = ((params_kw or {}).get("cluster_id") or "").strip()
|
||||
if not cluster_id:
|
||||
return {"widgettype": "Message", "options": {"title": "缺少参数", "message": "缺少 cluster_id"}}
|
||||
try:
|
||||
limit = int((params_kw or {}).get("limit") or 50)
|
||||
except (TypeError, ValueError):
|
||||
limit = 50
|
||||
|
||||
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 cluster_detail
|
||||
from pipeline_opportunity.opp_common import source_text
|
||||
_ok, _res = await cluster_detail(sor, _ctx, cluster_id, limit=limit)
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
|
||||
detail_url = entire_url("/pipeline-opportunity/api/opp_item_detail.dspy")
|
||||
|
||||
rows = []
|
||||
if not _ok:
|
||||
rows.append({"widgettype": "Text", "options": {
|
||||
"text": str(_res)[:200], "color": "#dc2626", "padding": "12px", "halign": "left"}})
|
||||
cl_name, cl_cnt = "", 0
|
||||
else:
|
||||
cl = _res.get("cluster") or {}
|
||||
cl_name = str(cl.get("name") or "")
|
||||
cl_cnt = int(cl.get("doc_count") or 0)
|
||||
items = _res.get("samples") or []
|
||||
if not items:
|
||||
rows.append({"widgettype": "Text", "options": {
|
||||
"text": "该类别暂无明细", "color": "#64748b", "padding": "12px", "halign": "left"}})
|
||||
for it in items:
|
||||
src_id = str(it.get("src_id") or "")
|
||||
title = str(it.get("title") or "")
|
||||
budget = it.get("budget_wan") or 0
|
||||
url = str(it.get("url") or "")
|
||||
src_name = source_text(str(it.get("source") or ""), "")
|
||||
meta = "%s | %s | 来源:%s" % (
|
||||
str(it.get("publish_time") or "")[:10],
|
||||
(("预算 %.2f 万" % float(budget)) if budget else "预算面议"),
|
||||
src_name)
|
||||
row_subs = [
|
||||
{"widgettype": "Text", "options": {"text": title, "cfontsize": 0.95,
|
||||
"color": "#0f172a", "halign": "left", "wrap": True}},
|
||||
{"widgettype": "HBox", "options": {"width": "100%", "gap": "10px", "alignItems": "center"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": meta, "cfontsize": 0.75,
|
||||
"color": "#94a3b8", "halign": "left", "wrap": True}},
|
||||
{"widgettype": "Filler"},
|
||||
{"widgettype": "Text", "options": {"text": "›", "cfontsize": 1.0, "color": "#94a3b8"}},
|
||||
]},
|
||||
]
|
||||
row_opts = {"width": "100%", "padding": "8px 12px", "gap": "4px",
|
||||
"border": "1px solid #e2e8f0", "borderRadius": "8px",
|
||||
"margin": "0 0 6px 0", "bgcolor": "#ffffff"}
|
||||
if src_id:
|
||||
row_opts["cursor"] = "pointer"
|
||||
script = ("var old=bricks.getWidgetById('opp_item_detail_pw',bricks.app);"
|
||||
"if(old&&old.destroy){old.destroy();}"
|
||||
"var rp=await fetch(" + _json.dumps(detail_url) + "+'?id='+encodeURIComponent(" + _json.dumps(src_id) + "));"
|
||||
"var d=await rp.json();if(d){bricks.widgetBuild(d,bricks.app);}")
|
||||
rows.append({"widgettype": "VBox", "options": row_opts,
|
||||
"binds": [{"wid": "self", "event": "click", "actiontype": "script",
|
||||
"target": "self", "script": script}],
|
||||
"subwidgets": row_subs})
|
||||
elif url:
|
||||
row_opts["cursor"] = "pointer"
|
||||
script = "window.open(" + _json.dumps(url) + ",'_blank');"
|
||||
rows.append({"widgettype": "VBox", "options": row_opts,
|
||||
"binds": [{"wid": "self", "event": "click", "actiontype": "script",
|
||||
"target": "self", "script": script}],
|
||||
"subwidgets": row_subs})
|
||||
else:
|
||||
rows.append({"widgettype": "VBox", "options": row_opts, "subwidgets": row_subs})
|
||||
|
||||
return {
|
||||
"widgettype": "PopupWindow",
|
||||
"id": "opp_mine_items_pw",
|
||||
"options": {"title": "类内需求明细:%s(%d 条,显示前 %d)" % (cl_name, cl_cnt, limit),
|
||||
"width": "76%", "height": "80%", "auto_open": True, "resizable": True},
|
||||
"subwidgets": [{
|
||||
"widgettype": "VBox",
|
||||
"options": {"css": "filler", "width": "100%", "height": "100%",
|
||||
"padding": "12px 16px", "gap": "0px", "overflow": "auto"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {
|
||||
"text": "点击任一条目弹出需求详情(数据来自数据爬取平台快照,每条标注来源平台)。",
|
||||
"cfontsize": 0.78, "color": "#64748b", "halign": "left", "margin": "0 0 8px 0"}},
|
||||
] + rows,
|
||||
}],
|
||||
}
|
||||
181
wwwroot/api/opp_mining_popup.dspy
Normal file
181
wwwroot/api/opp_mining_popup.dspy
Normal file
@ -0,0 +1,181 @@
|
||||
# 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,
|
||||
}
|
||||
48
wwwroot/api/opp_mining_start.dspy
Normal file
48
wwwroot/api/opp_mining_start.dspy
Normal file
@ -0,0 +1,48 @@
|
||||
# opp_mining_start.dspy - 启动需求挖掘批次(opp_mining_popup 的按钮端点)
|
||||
# GET/POST: scope=top|targeted, keyword(targeted必填), category?, days?, sources?
|
||||
# 复用 opp_mining_flow.start_mining(与工具通道同一实现),返回 {status,batch_id}。
|
||||
# 权限:logined;项目归属从会话当前项目解析(与列表页同一口径)。
|
||||
|
||||
uid = await get_user()
|
||||
if not uid:
|
||||
return {"status": "error", "message": "未登录"}
|
||||
|
||||
dbname = get_module_dbname('pipeline-opportunity')
|
||||
|
||||
scope = ((params_kw or {}).get("scope") or "top").strip()
|
||||
keyword = ((params_kw or {}).get("keyword") or "").strip()
|
||||
category = ((params_kw or {}).get("category") or "").strip()
|
||||
try:
|
||||
days = int((params_kw or {}).get("days") or 365)
|
||||
except (TypeError, ValueError):
|
||||
days = 365
|
||||
sources = ((params_kw or {}).get("sources") or "").strip()
|
||||
|
||||
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 start_mining
|
||||
bid, err = await start_mining(sor, _ctx, scope=scope, days=days, sources=sources,
|
||||
keyword=keyword, category=category)
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
|
||||
if err:
|
||||
return {"status": "error", "message": err}
|
||||
return {"status": "ok", "batch_id": bid}
|
||||
Loading…
x
Reference in New Issue
Block a user