feat(references): 分项明细列表支持排序——budget标的金额大到小/time发布时间后到前;数据源侧先全局排序再截断(金额TopN是真TopN非局部排序);三个分项弹窗(招标/国内众包/海外)顶部加排序切换按钮(当前态primary高亮,点击重建弹窗带sort参数);弹窗标题回显当前排序;agent工具opp_category_references/opp_overseas_references加sort参数(白名单收敛,非法值回落time)
This commit is contained in:
parent
7a8afab054
commit
684ec3ba69
@ -49,9 +49,10 @@ OPP_TOOLS = [
|
||||
parameters={"tender_id": "招标信息ID"}, category="data"),
|
||||
ToolDefinition(name="opp_crawler_stats", description="查看数据爬取平台采集健康度(总量/24h新增/最近运行)",
|
||||
parameters={}, category="data"),
|
||||
ToolDefinition(name="opp_category_references", description="查某软件主题的分项明细(每条带公告原文URL+采集来源,供逐条查证/举例)。⚠️ 明细是「举例佐证」用的,不是「统计」用的——排名/数量/金额统计先用 opp_hot_software(已返回 tender_count+total_budget_wan),只在需要给2~5个样例链接时才调本工具且 limit≤10",
|
||||
ToolDefinition(name="opp_category_references", description="查某软件主题的分项明细(每条带公告原文URL+采集来源,供逐条查证/举例;sort=budget 按标的金额大到小 / time 按发布时间后到前)。⚠️ 明细是「举例佐证」用的,不是「统计」用的——排名/数量/金额统计先用 opp_hot_software(已返回 tender_count+total_budget_wan),只在需要给2~5个样例链接时才调本工具且 limit≤10",
|
||||
parameters={"category": "软件主题名(须与热点排名中的category一致)",
|
||||
"days": "统计窗口天数(默认30)", "limit": "条数(默认20,最大50——只取样例不要拉全量)"}, category="data"),
|
||||
"days": "统计窗口天数(默认30)", "limit": "条数(默认20,最大50——只取样例不要拉全量)",
|
||||
"sort": "排序(可选: budget=标的金额大到小/time=时间后到前,默认time)"}, category="data"),
|
||||
ToolDefinition(name="opp_demands", description="查众包平台需求明细列表(国内猪八戒zbj/一品威客epwk + 海外freelancer/pph,每条带详情页URL可查证)。⚠️ 主题排名/数量/金额统计先用 opp_hot_demands(国内)/opp_hot_overseas(海外)——它们已聚合好排名;本工具只用于按关键词找具体需求样例,limit≤10",
|
||||
parameters={"keyword": "标题关键词(建议必填以缩小范围)", "days": "统计窗口天数(默认30)",
|
||||
"source": "来源(可选: zbj猪八戒/epwk一品威客/freelancer海外/pph海外,不传=全部)",
|
||||
@ -60,9 +61,10 @@ OPP_TOOLS = [
|
||||
parameters={"days": "统计窗口天数(默认30)", "top": "Top条数(默认10)"}, category="data"),
|
||||
ToolDefinition(name="opp_hot_overseas", description="【统计首选】海外众包需求热点主题排名(Freelancer/PeoplePerHour,英文标题按8大主题归类)——直接返回各主题数量+预算合计+按来源分布,无需拉明细自己数。用户问国外/海外市场、中外众包排名时用本工具",
|
||||
parameters={"days": "统计窗口天数(默认30)", "top": "Top条数(默认10)"}, category="data"),
|
||||
ToolDefinition(name="opp_overseas_references", description="查某海外需求主题的分项明细(每条带Freelancer/PeoplePerHour详情页URL,供逐条查证/举例)。⚠️ 统计先用 opp_hot_overseas(已返回 tender_count+total_budget_wan+by_source),只在需要2~5个样例链接时才调本工具且 limit≤10",
|
||||
ToolDefinition(name="opp_overseas_references", description="查某海外需求主题的分项明细(每条带Freelancer/PeoplePerHour详情页URL,供逐条查证/举例;sort=budget 按赏金大到小 / time 按发布时间后到前)。⚠️ 统计先用 opp_hot_overseas(已返回 tender_count+total_budget_wan+by_source),只在需要2~5个样例链接时才调本工具且 limit≤10",
|
||||
parameters={"category": "海外主题名(须与 opp_hot_overseas 排名中的category一致)",
|
||||
"days": "统计窗口天数(默认30)", "limit": "条数(默认20,最大50——只取样例不要拉全量)"}, category="data"),
|
||||
"days": "统计窗口天数(默认30)", "limit": "条数(默认20,最大50——只取样例不要拉全量)",
|
||||
"sort": "排序(可选: budget=赏金大到小/time=时间后到前,默认time)"}, category="data"),
|
||||
|
||||
# ── 报告与审批(商机闭环)──
|
||||
ToolDefinition(name="opp_create_report", description="创建软件研发报告草稿。从热门软件中选定的方向写入报告,数据基础章节自动从爬取平台拉取。正文必须按四节模板组织(## 研发场景 / ## 输入 / ## 输出 / ## 成果),商店详情页按节展示",
|
||||
@ -337,13 +339,16 @@ async def _h_category_references(sor, p, ctx):
|
||||
return _fmt(False, "缺少 category(软件主题名)")
|
||||
ok, res = await category_references(
|
||||
sor, category, days=int(p.get("days") or 30),
|
||||
limit=min(int(p.get("limit") or 200), 1000))
|
||||
limit=min(int(p.get("limit") or 200), 1000),
|
||||
sort=str(p.get("sort") or "").strip())
|
||||
if not ok:
|
||||
return _fmt(False, res)
|
||||
return json.dumps({
|
||||
"category": res.get("category"),
|
||||
"窗口": "%d天" % res.get("window_days"),
|
||||
"分项总数": res.get("count"),
|
||||
"排序": {"budget": "标的金额大到小", "time": "发布时间后到前"}.get(
|
||||
res.get("sort") or "time", "发布时间后到前"),
|
||||
"分项": res.get("items"),
|
||||
"说明": "每条分项的 url 为公告原文、source_url 为采集来源,均可点击查证",
|
||||
}, ensure_ascii=False, default=str)
|
||||
@ -393,7 +398,8 @@ async def _h_overseas_references(sor, p, ctx):
|
||||
return _fmt(False, {"error": "缺少 category 参数"})
|
||||
ok, res = await overseas_references(
|
||||
sor, category=category, days=int(p.get("days") or 30),
|
||||
limit=min(int(p.get("limit") or 200), 500))
|
||||
limit=min(int(p.get("limit") or 200), 500),
|
||||
sort=str(p.get("sort") or "").strip())
|
||||
if not ok:
|
||||
return _fmt(False, res)
|
||||
return json.dumps(res, ensure_ascii=False, default=str)
|
||||
|
||||
@ -55,17 +55,22 @@ async def hot_software(sor, days=30, top=10):
|
||||
{"days": days, "top": top})
|
||||
|
||||
|
||||
async def category_references(sor, category, days=30, limit=20, record_type="tender"):
|
||||
async def category_references(sor, category, days=30, limit=20, record_type="tender",
|
||||
sort=""):
|
||||
"""某软件主题的分项明细(references 可查证,举例佐证用)。
|
||||
|
||||
每条带公告原文/详情页 url + 采集来源 source_url,供界面逐条查证。
|
||||
record_type: tender=招标(默认) / demand=众包需求发布(猪八戒/一品威客)。
|
||||
sort: ""=缺省(时间后到前) / time=发布时间后到前 / budget=标的金额大到小。
|
||||
排序在数据源侧全量执行后再截断(「金额前 N」是全窗口真 TopN)。
|
||||
limit 硬上限 _DETAIL_LIMIT_MAX(50)——统计用 hot_software 聚合,勿拉全量明细。
|
||||
"""
|
||||
return await crawler_get(sor, "/api/category_references",
|
||||
{"category": category, "days": days,
|
||||
"limit": _clamp_detail_limit(limit),
|
||||
"record_type": record_type})
|
||||
params = {"category": category, "days": days,
|
||||
"limit": _clamp_detail_limit(limit),
|
||||
"record_type": record_type}
|
||||
if sort:
|
||||
params["sort"] = sort
|
||||
return await crawler_get(sor, "/api/category_references", params)
|
||||
|
||||
|
||||
async def crawler_stats(sor):
|
||||
@ -99,15 +104,18 @@ async def hot_overseas(sor, days=30, top=10):
|
||||
{"days": days, "top": top})
|
||||
|
||||
|
||||
async def overseas_references(sor, category, days=30, limit=20):
|
||||
async def overseas_references(sor, category, days=30, limit=20, sort=""):
|
||||
"""某海外需求主题的分项明细(references 可查证,举例佐证用)。
|
||||
|
||||
每条带海外平台详情页 url,供界面逐条查证。
|
||||
sort: ""=缺省(时间后到前) / time=发布时间后到前 / budget=赏金(折人民币)大到小。
|
||||
limit 硬上限 _DETAIL_LIMIT_MAX(50)——统计用 hot_overseas 聚合,勿拉全量明细。
|
||||
"""
|
||||
return await crawler_get(sor, "/api/overseas_references",
|
||||
{"category": category, "days": days,
|
||||
"limit": _clamp_detail_limit(limit)})
|
||||
params = {"category": category, "days": days,
|
||||
"limit": _clamp_detail_limit(limit)}
|
||||
if sort:
|
||||
params["sort"] = sort
|
||||
return await crawler_get(sor, "/api/overseas_references", params)
|
||||
|
||||
|
||||
async def query_tenders(sor, keyword="", region="", buyer="", notice_type="",
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# opp_demand_items.dspy - 某软件主题的众包需求分项列表(国内猪八戒/一品威客)
|
||||
# 入参:category=<主题名>。交互(2026-09-08 用户定夺):
|
||||
# 入参:category=<主题名>,sort=time|budget(2026-09-15 用户需求:支持按赏金
|
||||
# 大到小 / 按时间后到前排序;排序在数据源侧全量执行后再截断,缺省 time)。
|
||||
# 交互(2026-09-08 用户定夺):
|
||||
# 每条分项整行点击 → opp_item_detail.dspy 弹出具体内容(爬取平台数据+附件下载);
|
||||
# 来源写进每条说明文字(来源:猪八戒/一品威客),不再渲染来源按钮。
|
||||
|
||||
@ -13,17 +15,44 @@ category = ((params_kw or {}).get("category") or "").strip()
|
||||
days = int((params_kw or {}).get("days") or 30)
|
||||
if not category:
|
||||
return {"widgettype": "Message", "options": {"title": "缺少参数", "message": "缺少 category"}}
|
||||
# 排序白名单收敛(非法/缺省一律 time=发布时间后到前,与历史行为一致)
|
||||
sort = ((params_kw or {}).get("sort") or "time").strip()
|
||||
if sort not in ("time", "budget"):
|
||||
sort = "time"
|
||||
|
||||
dbname = get_module_dbname('pipeline-opportunity')
|
||||
|
||||
detail_url = entire_url("/pipeline-opportunity/api/opp_item_detail.dspy")
|
||||
self_url = entire_url("/pipeline-opportunity/api/opp_demand_items.dspy")
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
from pipeline_opportunity.opp_data_capability import category_references
|
||||
from pipeline_opportunity.opp_common import source_text
|
||||
ok, res = await category_references(sor, category, days=days, limit=500, record_type="demand")
|
||||
ok, res = await category_references(sor, category, days=days, limit=500,
|
||||
record_type="demand", sort=sort)
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
|
||||
# ── 排序切换按钮(点击重建弹窗带 sort 参数;对齐 opp_mining_popup 刷新范式)──
|
||||
def _sort_btn(label, target_sort, active):
|
||||
script = ("var old=bricks.getWidgetById('opp_demand_items_pw',bricks.app);"
|
||||
"if(old&&old.destroy){old.destroy();}"
|
||||
"var rp=await fetch(" + _json.dumps(self_url) + "+'?category='+encodeURIComponent("
|
||||
+ _json.dumps(category) + ")+'&days=" + str(days) + "&sort=" + target_sort + "');"
|
||||
"var d=await rp.json();"
|
||||
"if(d){bricks.widgetBuild(d,bricks.app);}")
|
||||
return {"widgettype": "Button",
|
||||
"options": {"label": label, "css": ("primary" if active else "small")},
|
||||
"binds": [{"wid": "self", "event": "click", "actiontype": "script",
|
||||
"target": "self", "script": script}]}
|
||||
|
||||
sort_bar = {"widgettype": "HBox", "options": {"width": "100%", "gap": "8px",
|
||||
"alignItems": "center", "margin": "0 0 8px 0"},
|
||||
"subwidgets": [
|
||||
_sort_btn("💰 赏金(大到小)", "budget", sort == "budget"),
|
||||
_sort_btn("⏱ 发布时间(后到前)", "time", sort == "time"),
|
||||
{"widgettype": "Filler"},
|
||||
]}
|
||||
|
||||
rows = []
|
||||
if not ok:
|
||||
rows.append({"widgettype": "Text", "options": {
|
||||
@ -79,16 +108,17 @@ else:
|
||||
"subwidgets": row_subs})
|
||||
|
||||
total = res.get("count", len(rows)) if ok else 0
|
||||
_sort_label = "赏金大到小" if sort == "budget" else "时间后到前"
|
||||
return {
|
||||
"widgettype": "PopupWindow",
|
||||
"id": "opp_demand_items_pw",
|
||||
"options": {"title": "众包需求分项:%s(%d 条)" % (category, total),
|
||||
"options": {"title": "众包需求分项:%s(%d 条 · %s)" % (category, total, _sort_label),
|
||||
"width": "80%", "height": "82%", "auto_open": True, "resizable": True},
|
||||
"subwidgets": [{
|
||||
"widgettype": "VBox",
|
||||
"options": {"css": "filler", "width": "100%", "height": "100%",
|
||||
"padding": "12px 16px", "gap": "0px", "overflow": "auto"},
|
||||
"subwidgets": [
|
||||
"subwidgets": [sort_bar,
|
||||
{"widgettype": "Text", "options": {
|
||||
"otext": "点击任一条目弹出具体内容(数据来自数据爬取平台);每条说明中已标注来源平台。", "text": "点击任一条目弹出具体内容(数据来自数据爬取平台);每条说明中已标注来源平台。", "i18n": True,
|
||||
"cfontsize": 0.78, "color": "#64748b", "halign": "left", "margin": "0 0 8px 0"}}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# opp_overseas_items.dspy - 某海外需求主题的分项列表(references 可查证)
|
||||
# 入参:category=<海外主题名>。交互(2026-09-08 用户定夺):
|
||||
# 入参:category=<海外主题名>,sort=time|budget(2026-09-15 用户需求:支持按赏金
|
||||
# (折人民币)大到小 / 按时间后到前排序;排序在数据源侧全量执行后再截断,缺省 time)。
|
||||
# 交互(2026-09-08 用户定夺):
|
||||
# 每条分项整行点击 → opp_item_detail.dspy 弹出具体内容(爬取平台数据+附件下载);
|
||||
# 来源写进每条说明文字(来源:Freelancer.com / PeoplePerHour),不再渲染来源按钮。
|
||||
|
||||
@ -13,17 +15,43 @@ category = ((params_kw or {}).get("category") or "").strip()
|
||||
days = int((params_kw or {}).get("days") or 30)
|
||||
if not category:
|
||||
return {"widgettype": "Message", "options": {"title": "缺少参数", "message": "缺少 category"}}
|
||||
# 排序白名单收敛(非法/缺省一律 time=发布时间后到前,与历史行为一致)
|
||||
sort = ((params_kw or {}).get("sort") or "time").strip()
|
||||
if sort not in ("time", "budget"):
|
||||
sort = "time"
|
||||
|
||||
dbname = get_module_dbname('pipeline-opportunity')
|
||||
|
||||
detail_url = entire_url("/pipeline-opportunity/api/opp_item_detail.dspy")
|
||||
self_url = entire_url("/pipeline-opportunity/api/opp_overseas_items.dspy")
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
from pipeline_opportunity.opp_data_capability import overseas_references
|
||||
from pipeline_opportunity.opp_common import source_text
|
||||
ok, res = await overseas_references(sor, category, days=days, limit=500)
|
||||
ok, res = await overseas_references(sor, category, days=days, limit=500, sort=sort)
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
|
||||
# ── 排序切换按钮(点击重建弹窗带 sort 参数;对齐 opp_mining_popup 刷新范式)──
|
||||
def _sort_btn(label, target_sort, active):
|
||||
script = ("var old=bricks.getWidgetById('opp_overseas_items_pw',bricks.app);"
|
||||
"if(old&&old.destroy){old.destroy();}"
|
||||
"var rp=await fetch(" + _json.dumps(self_url) + "+'?category='+encodeURIComponent("
|
||||
+ _json.dumps(category) + ")+'&days=" + str(days) + "&sort=" + target_sort + "');"
|
||||
"var d=await rp.json();"
|
||||
"if(d){bricks.widgetBuild(d,bricks.app);}")
|
||||
return {"widgettype": "Button",
|
||||
"options": {"label": label, "css": ("primary" if active else "small")},
|
||||
"binds": [{"wid": "self", "event": "click", "actiontype": "script",
|
||||
"target": "self", "script": script}]}
|
||||
|
||||
sort_bar = {"widgettype": "HBox", "options": {"width": "100%", "gap": "8px",
|
||||
"alignItems": "center", "margin": "0 0 8px 0"},
|
||||
"subwidgets": [
|
||||
_sort_btn("💰 赏金(大到小)", "budget", sort == "budget"),
|
||||
_sort_btn("⏱ 发布时间(后到前)", "time", sort == "time"),
|
||||
{"widgettype": "Filler"},
|
||||
]}
|
||||
|
||||
rows = []
|
||||
if not ok:
|
||||
rows.append({"widgettype": "Text", "options": {
|
||||
@ -79,16 +107,17 @@ else:
|
||||
"subwidgets": row_subs})
|
||||
|
||||
total = res.get("matched", len(rows)) if ok else 0
|
||||
_sort_label = "赏金大到小" if sort == "budget" else "时间后到前"
|
||||
return {
|
||||
"widgettype": "PopupWindow",
|
||||
"id": "opp_overseas_items_pw",
|
||||
"options": {"title": "海外需求分项:%s(%d 条)" % (category, total),
|
||||
"options": {"title": "海外需求分项:%s(%d 条 · %s)" % (category, total, _sort_label),
|
||||
"width": "80%", "height": "82%", "auto_open": True, "resizable": True},
|
||||
"subwidgets": [{
|
||||
"widgettype": "VBox",
|
||||
"options": {"css": "filler", "width": "100%", "height": "100%",
|
||||
"padding": "12px 16px", "gap": "0px", "overflow": "auto"},
|
||||
"subwidgets": [
|
||||
"subwidgets": [sort_bar,
|
||||
{"widgettype": "Text", "options": {
|
||||
"otext": "点击任一条目弹出具体内容(数据来自数据爬取平台);每条说明中已标注来源平台。", "text": "点击任一条目弹出具体内容(数据来自数据爬取平台);每条说明中已标注来源平台。", "i18n": True,
|
||||
"cfontsize": 0.78, "color": "#64748b", "halign": "left", "margin": "0 0 8px 0"}}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# opp_references_items.dspy - 某软件主题的分项列表(references 可查证)
|
||||
# 入参:category=<主题名>。交互(2026-09-08 用户定夺):
|
||||
# 入参:category=<主题名>,sort=time|budget(2026-09-15 用户需求:支持按标的金额
|
||||
# 大到小 / 按时间后到前排序;排序在数据源侧全量执行后再截断,缺省 time)。
|
||||
# 交互(2026-09-08 用户定夺):
|
||||
# 每条分项整行点击 → opp_item_detail.dspy 弹出具体内容(爬取平台全文+附件下载);
|
||||
# 来源写进每条说明文字(来源:xxx),不再渲染来源按钮。
|
||||
|
||||
@ -13,17 +15,43 @@ category = ((params_kw or {}).get("category") or "").strip()
|
||||
days = int((params_kw or {}).get("days") or 30)
|
||||
if not category:
|
||||
return {"widgettype": "Message", "options": {"title": "缺少参数", "message": "缺少 category"}}
|
||||
# 排序白名单收敛(非法/缺省一律 time=发布时间后到前,与历史行为一致)
|
||||
sort = ((params_kw or {}).get("sort") or "time").strip()
|
||||
if sort not in ("time", "budget"):
|
||||
sort = "time"
|
||||
|
||||
dbname = get_module_dbname('pipeline-opportunity')
|
||||
|
||||
detail_url = entire_url("/pipeline-opportunity/api/opp_item_detail.dspy")
|
||||
self_url = entire_url("/pipeline-opportunity/api/opp_references_items.dspy")
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
from pipeline_opportunity.opp_data_capability import category_references
|
||||
from pipeline_opportunity.opp_common import source_text
|
||||
ok, res = await category_references(sor, category, days=days, limit=500)
|
||||
ok, res = await category_references(sor, category, days=days, limit=500, sort=sort)
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
|
||||
# ── 排序切换按钮(点击重建弹窗带 sort 参数;对齐 opp_mining_popup 刷新范式)──
|
||||
def _sort_btn(label, target_sort, active):
|
||||
script = ("var old=bricks.getWidgetById('opp_refs_items_pw',bricks.app);"
|
||||
"if(old&&old.destroy){old.destroy();}"
|
||||
"var rp=await fetch(" + _json.dumps(self_url) + "+'?category='+encodeURIComponent("
|
||||
+ _json.dumps(category) + ")+'&days=" + str(days) + "&sort=" + target_sort + "');"
|
||||
"var d=await rp.json();"
|
||||
"if(d){bricks.widgetBuild(d,bricks.app);}")
|
||||
return {"widgettype": "Button",
|
||||
"options": {"label": label, "css": ("primary" if active else "small")},
|
||||
"binds": [{"wid": "self", "event": "click", "actiontype": "script",
|
||||
"target": "self", "script": script}]}
|
||||
|
||||
sort_bar = {"widgettype": "HBox", "options": {"width": "100%", "gap": "8px",
|
||||
"alignItems": "center", "margin": "0 0 8px 0"},
|
||||
"subwidgets": [
|
||||
_sort_btn("💰 标的金额(大到小)", "budget", sort == "budget"),
|
||||
_sort_btn("⏱ 发布时间(后到前)", "time", sort == "time"),
|
||||
{"widgettype": "Filler"},
|
||||
]}
|
||||
|
||||
rows = []
|
||||
if not ok:
|
||||
rows.append({"widgettype": "Text", "options": {
|
||||
@ -84,16 +112,17 @@ else:
|
||||
"subwidgets": row_subs})
|
||||
|
||||
total = res.get("count", len(rows)) if ok else 0
|
||||
_sort_label = "标的金额大到小" if sort == "budget" else "时间后到前"
|
||||
return {
|
||||
"widgettype": "PopupWindow",
|
||||
"id": "opp_refs_items_pw",
|
||||
"options": {"title": "分项明细:%s(%d 条)" % (category, total),
|
||||
"options": {"title": "分项明细:%s(%d 条 · %s)" % (category, total, _sort_label),
|
||||
"width": "80%", "height": "82%", "auto_open": True, "resizable": True},
|
||||
"subwidgets": [{
|
||||
"widgettype": "VBox",
|
||||
"options": {"css": "filler", "width": "100%", "height": "100%",
|
||||
"padding": "12px 16px", "gap": "0px", "overflow": "auto"},
|
||||
"subwidgets": [
|
||||
"subwidgets": [sort_bar,
|
||||
{"widgettype": "Text", "options": {
|
||||
"otext": "点击任一条目弹出具体内容(公告全文来自数据爬取平台,有附件的可直接下载);每条说明中已标注数据来源。", "text": "点击任一条目弹出具体内容(公告全文来自数据爬取平台,有附件的可直接下载);每条说明中已标注数据来源。", "i18n": True,
|
||||
"cfontsize": 0.78, "color": "#64748b", "halign": "left", "margin": "0 0 8px 0"}}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user