海外软件需求接入商机产线: opp_hot_overseas/opp_overseas_references工具+数据参考弹窗海外板块+opp_demands补海外源

This commit is contained in:
yumoqing 2026-08-30 19:54:00 +08:00
parent a53b81ffe6
commit ca8e9f2c71
5 changed files with 181 additions and 6 deletions

View File

@ -52,12 +52,17 @@ OPP_TOOLS = [
ToolDefinition(name="opp_category_references", description="查某软件主题的全部分项明细references 可查证每条带公告原文URL+采集来源)。回答热点/排名类问题时用本工具取分项并附来源链接",
parameters={"category": "软件主题名(须与热点排名中的category一致)",
"days": "统计窗口天数(默认30)", "limit": "条数(默认200)"}, category="data"),
ToolDefinition(name="opp_demands", description="查众包平台(猪八戒/一品威客)软件需求发布列表——甲方直接发布的开发需求,反映真实市场需求与预算分布。每条带详情页URL可查证",
ToolDefinition(name="opp_demands", description="查众包平台软件需求发布列表——甲方直接发布的开发需求,覆盖国内猪八戒zbj/一品威客epwk与海外Freelancer.com freelancer/PeoplePerHour pph。每条带详情页URL可查证",
parameters={"keyword": "标题关键词(可选)", "days": "统计窗口天数(默认30)",
"source": "来源(可选: zbj猪八戒/epwk一品威客,不传=全部)",
"source": "来源(可选: zbj猪八戒/epwk一品威客/freelancer海外/pph海外,不传=全部)",
"limit": "条数(默认50)"}, category="data"),
ToolDefinition(name="opp_hot_demands", description="众包需求热点主题排名(需求侧热度,与招标侧的 opp_hot_software 互补)",
ToolDefinition(name="opp_hot_demands", description="国内众包需求热点主题排名(需求侧热度,与招标侧的 opp_hot_software 互补)",
parameters={"days": "统计窗口天数(默认30)", "top": "Top条数(默认10)"}, category="data"),
ToolDefinition(name="opp_hot_overseas", description="海外软件需求热点主题排名Freelancer.com/PeoplePerHour 众包需求英文标题按8大主题归类AI智能体/Web/移动/桌面企业软件/数据集成/自动化/DevOps云/游戏)。用户问国外/海外市场、国外软件需求前十时用本工具",
parameters={"days": "统计窗口天数(默认30)", "top": "Top条数(默认10)"}, category="data"),
ToolDefinition(name="opp_overseas_references", description="查某海外需求主题的全部分项明细references 可查证每条带Freelancer/PeoplePerHour详情页URL。回答海外热点问题时用本工具取分项并附来源链接",
parameters={"category": "海外主题名(须与 opp_hot_overseas 排名中的category一致)",
"days": "统计窗口天数(默认30)", "limit": "条数(默认200)"}, category="data"),
# ── 报告与审批(商机闭环)──
ToolDefinition(name="opp_create_report", description="创建软件研发报告草稿。从热门软件中选定的方向写入报告,数据基础章节自动从爬取平台拉取",
@ -130,6 +135,10 @@ opp_query_tenders / opp_tender_detail**不要**自己去爬网站,
- 用户问今天有什么AI招标 opp_daily_ai_tenders()
- 用户问市场真实需求/甲方都在找什么开发 opp_hot_demands opp_demands
猪八戒/一品威客等众包平台的甲方需求发布与招标互补同样带 references
- 用户问国外/海外市场软件需求国外软件需求前十 opp_hot_overseas
Freelancer.com/PeoplePerHour 海外众包需求英文标题已按8大主题归类
并对感兴趣的主题调 opp_overseas_references 附海外平台详情页 references
注意本产线已覆盖海外市场freelancer/pph 两源不许回答"没有海外数据"
- 用户说给XX写个研发报告 先确认调研充分1-3
opp_create_report 拉数据补写 opp_submit_report
- 用户问进展 opp_diagnose报告/审批分布 + 待办人工任务"""
@ -295,6 +304,29 @@ async def _h_hot_demands(sor, p, ctx):
return json.dumps(res, ensure_ascii=False, default=str)
async def _h_hot_overseas(sor, p, ctx):
from .opp_data_capability import hot_overseas
ok, res = await hot_overseas(
sor, days=int(p.get("days") or 30),
top=min(int(p.get("top") or 10), 30))
if not ok:
return _fmt(False, res)
return json.dumps(res, ensure_ascii=False, default=str)
async def _h_overseas_references(sor, p, ctx):
from .opp_data_capability import overseas_references
category = (p.get("category") or "").strip()
if not category:
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))
if not ok:
return _fmt(False, res)
return json.dumps(res, ensure_ascii=False, default=str)
async def _h_create_report(sor, p, ctx):
from .opp_report_capability import create_report
ok, rid = await create_report(
@ -373,6 +405,8 @@ OPP_HANDLERS = {
"opp_category_references": _h_category_references,
"opp_demands": _h_demands,
"opp_hot_demands": _h_hot_demands,
"opp_hot_overseas": _h_hot_overseas,
"opp_overseas_references": _h_overseas_references,
"opp_create_report": _h_create_report,
"opp_update_report": _h_update_report,
"opp_list_reports": _h_list_reports,

View File

@ -52,7 +52,7 @@ async def crawler_stats(sor):
async def demands(sor, keyword="", days=30, source="", limit=50):
"""众包平台软件需求发布列表(猪八戒/一品威客等)。
"""众包平台软件需求发布列表(国内:猪八戒/一品威客海外Freelancer/PeoplePerHour)。
区别于招标这些是甲方在众包平台直接发布的开发需求
反映市场真实需求热度与预算分布每条带详情页 url可查证
@ -71,6 +71,21 @@ async def hot_demands(sor, days=30, top=10):
{"days": days, "top": top})
async def hot_overseas(sor, days=30, top=10):
"""海外众包软件需求热点主题排名Freelancer/PeoplePerHour英文主题"""
return await crawler_get(sor, "/api/hot_overseas",
{"days": days, "top": top})
async def overseas_references(sor, category, days=30, limit=500):
"""某海外需求主题的全部分项references 可查证)。
每条带海外平台详情页 url供界面逐条查证
"""
return await crawler_get(sor, "/api/overseas_references",
{"category": category, "days": days, "limit": limit})
async def query_tenders(sor, keyword="", region="", buyer="", notice_type="",
source="", min_budget="", max_budget="", min_ai="",
days=90, group_by="", limit=50, only_it=True):

View File

@ -26,6 +26,7 @@ PATHS_LOGINED = [
"/%s/agent/index.ui" % MOD,
"/%s/api/opp_references_popup.dspy" % MOD,
"/%s/api/opp_references_items.dspy" % MOD,
"/%s/api/opp_overseas_items.dspy" % MOD,
"/%s/api/opp_confirm_report.dspy" % MOD,
"/%s/api/opp_report_ppt.dspy" % MOD,
]

View File

@ -0,0 +1,83 @@
# opp_overseas_items.dspy - 某海外需求主题的分项列表references 可查证)
# 入参category=<海外主题名>。每条分项:标题+时间+来源平台 + 「需求详情」按钮(新窗口)
import json as _json
uid = await get_user()
if not uid:
return {"widgettype": "Message", "options": {"title": "未登录", "message": "请先登录"}}
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"}}
dbname = get_module_dbname('pipeline-opportunity')
async with DBPools().sqlorContext(dbname) as sor:
from pipeline_opportunity.opp_data_capability import overseas_references
ok, res = await overseas_references(sor, category, days=days, limit=500)
await sor.sqlExe("COMMIT", {})
SRC_NAME = {"freelancer": "Freelancer.com", "pph": "PeoplePerHour"}
rows = []
if not ok:
rows.append({"widgettype": "Text", "options": {
"text": "查询失败:" + str(res)[:200],
"color": "#dc2626", "padding": "12px", "halign": "left"}})
else:
items = res.get("items") or []
if not items:
rows.append({"widgettype": "Text", "options": {
"text": "该主题近 %d 天无海外分项" % days, "color": "#64748b", "padding": "12px", "halign": "left"}})
for it in items:
url = str(it.get("url") or "")
src = SRC_NAME.get(str(it.get("source") or ""), str(it.get("source") or "海外"))
sub_btns = []
if url:
sub_btns.append({"widgettype": "Button", "options": {"label": "需求详情", "css": "small"},
"binds": [{"wid": "self", "event": "click", "actiontype": "script",
"target": "self",
"script": "window.open(" + _json.dumps(url) + ",'_blank','noopener');"}]})
budget = it.get("budget_wan") or 0
meta = "%s %s %s" % (
str(it.get("publish_time") or "")[:10],
src,
(("预算 %.2f 万(折人民币)" % budget) if budget else "预算未公开"))
rows.append({
"widgettype": "VBox",
"options": {"width": "100%", "padding": "8px 12px", "gap": "4px",
"border": "1px solid #e2e8f0", "borderRadius": "8px",
"margin": "0 0 6px 0", "bgcolor": "#ffffff"},
"subwidgets": [
{"widgettype": "Text", "options": {
"text": str(it.get("title") or ""), "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"}},
{"widgettype": "Filler"}
] + sub_btns}
]
})
total = res.get("matched", len(rows)) if ok else 0
return {
"widgettype": "PopupWindow",
"id": "opp_overseas_items_pw",
"options": {"title": "海外需求分项:%s%d 条)" % (category, total),
"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": [
{"widgettype": "Text", "options": {
"text": "每条「需求详情」为海外众包平台Freelancer.com / PeoplePerHour原始需求页在新窗口打开可逐条查证。",
"cfontsize": 0.78, "color": "#64748b", "halign": "left", "margin": "0 0 8px 0"}}
] + rows
}]
}

View File

@ -10,11 +10,14 @@ if not uid:
dbname = get_module_dbname('pipeline-opportunity')
items_url = entire_url("/pipeline-opportunity/api/opp_references_items.dspy")
overseas_items_url = entire_url("/pipeline-opportunity/api/opp_overseas_items.dspy")
async with DBPools().sqlorContext(dbname) as sor:
from pipeline_opportunity.opp_data_capability import hot_software
from pipeline_opportunity.opp_data_capability import hot_software, hot_overseas
ok, res = await hot_software(sor, days=30, top=10)
await sor.sqlExe("COMMIT", {})
ok_ov, res_ov = await hot_overseas(sor, days=30, top=10)
await sor.sqlExe("COMMIT", {})
rows = []
if not ok:
@ -54,6 +57,45 @@ else:
]
})
# ── 海外众包需求板块Freelancer.com / PeoplePerHour──
ov_rows = []
if ok_ov:
ov_ranking = res_ov.get("ranking") or []
if ov_ranking:
ov_rows.append({"widgettype": "Text", "options": {
"text": "海外众包软件需求Freelancer.com / PeoplePerHour近30天",
"cfontsize": 0.95, "color": "#0f172a", "halign": "left",
"margin": "14px 0 6px 0", "padding": "6px 0 0 0",
"border": "1px solid #f1f5f9", "borderRadius": "6px", "bgcolor": "#f8fafc"}})
for r in ov_ranking:
cat = str(r.get("category", ""))
cnt = r.get("tender_count", 0)
by_src = r.get("by_source") or {}
src_txt = " / ".join("%s:%d" % (k, v) for k, v in by_src.items())
script = ("var old=bricks.getWidgetById('opp_overseas_items_pw',bricks.app);"
"if(old&&old.destroy){old.destroy();}"
"var rp=await fetch(" + _json.dumps(overseas_items_url) + "+'?category='+encodeURIComponent(" + _json.dumps(cat) + "));"
"var d=await rp.json();"
"if(d){bricks.widgetBuild(d,bricks.app);}")
ov_rows.append({
"widgettype": "HBox",
"options": {"width": "100%", "gap": "10px", "alignItems": "center",
"padding": "8px 14px", "border": "1px solid #e2e8f0",
"borderRadius": "8px", "margin": "0 0 8px 0", "bgcolor": "#ffffff"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": cat, "cfontsize": 1.0,
"color": "#0f172a", "halign": "left"}},
{"widgettype": "Filler"},
{"widgettype": "Text", "options": {"text": "%d 条" % cnt,
"cfontsize": 0.85, "color": "#64748b"}},
{"widgettype": "Text", "options": {"text": src_txt,
"cfontsize": 0.75, "color": "#94a3b8"}},
{"widgettype": "Button", "options": {"label": "查看分项", "css": "small"},
"binds": [{"wid": "self", "event": "click", "actiontype": "script",
"target": "self", "script": script}]}
]
})
return {
"widgettype": "VBox",
"options": {"css": "filler", "width": "100%", "height": "100%", "padding": "12px 16px",
@ -62,5 +104,5 @@ return {
{"widgettype": "Text", "options": {
"text": "点击「查看分项」列出该主题的全部招标分项;每条分项带公告原文链接,可逐条查证。",
"cfontsize": 0.8, "color": "#64748b", "halign": "left", "margin": "0 0 8px 0"}}
] + rows
] + rows + ov_rows
}