pipeline-opportunity/wwwroot/api/opp_mining_popup.dspy
yumoqing 4092304726 feat(feasibility): P3可行性研究——opp_feasibility引擎(fp_estimate规则引擎FP/draft_feasibility八节报告/score_candidate评分/promote_to_project立项门禁)+report_type列(models/DDL/CRUD重生成)+详情页与PPT按类型选四节或八节模板(sections_for_type单一规则源)+列表类型徽标+prompt链路C+params/demand_mining字典种子+i18n词条
- fp_calc.calc()返回键实锤核对: total_fp/counts/errors(原total_ufp/by_type兜底猜错,已修)
- promote门禁修正: resolve_approval会把status置approved,confirmed只是中间态→门禁接受confirmed/approval_initiated/approved(原只认confirmed会导致审批通过后反而不能立项)
- _cluster_ctx补项目隔离(对齐_check_batch_visible口径)+全personal类别如实拒绝可行性样本不足
- 八节模板兜底节=需求规格;四节存量解析口径零变化
- opp_approvals CRUD手改(项目过滤)被xls2ui重生成吃掉→已checkout还原,只保留opp_reports新列部分
2026-09-14 09:03:58 +08:00

180 lines
9.0 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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": {
"otext": "需求挖掘:把众包平台(猪八戒/一品威客/Freelancer/PPH)的软件需求做语义聚类,发现市场热门软件类别(TopX)或分析你指定的需求类型。批次后台执行,完成后点击行查看类别排名。", "text": "需求挖掘:把众包平台(猪八戒/一品威客/Freelancer/PPH)的软件需求做语义聚类,发现市场热门软件类别(TopX)或分析你指定的需求类型。批次后台执行,完成后点击行查看类别排名。", "i18n": True,
"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": {
"otext": "查询失败:${p0}", "text": "查询失败:${p0}", "i18n": True, "i18n_params": {"p0": str(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": {
"otext": "还没有挖掘批次。点击上方「⛏ 启动全量挖掘」开始(首次全量约3-5分钟;embedding 有全机构共享缓存,二次批次显著更快)。", "text": "还没有挖掘批次。点击上方「⛏ 启动全量挖掘」开始(首次全量约3-5分钟;embedding 有全机构共享缓存,二次批次显著更快)。", "i18n": True,
"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,
}