From ca8e9f2c717d7c8206a702cd900f60ae2d4054e0 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sun, 30 Aug 2026 19:54:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=B7=E5=A4=96=E8=BD=AF=E4=BB=B6=E9=9C=80?= =?UTF-8?q?=E6=B1=82=E6=8E=A5=E5=85=A5=E5=95=86=E6=9C=BA=E4=BA=A7=E7=BA=BF?= =?UTF-8?q?:=20opp=5Fhot=5Foverseas/opp=5Foverseas=5Freferences=E5=B7=A5?= =?UTF-8?q?=E5=85=B7+=E6=95=B0=E6=8D=AE=E5=8F=82=E8=80=83=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E6=B5=B7=E5=A4=96=E6=9D=BF=E5=9D=97+opp=5Fdemands?= =?UTF-8?q?=E8=A1=A5=E6=B5=B7=E5=A4=96=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline_opportunity/opp_ability.py | 40 +++++++++- pipeline_opportunity/opp_data_capability.py | 17 ++++- scripts/load_path.py | 1 + wwwroot/api/opp_overseas_items.dspy | 83 +++++++++++++++++++++ wwwroot/api/opp_references_popup.dspy | 46 +++++++++++- 5 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 wwwroot/api/opp_overseas_items.dspy diff --git a/pipeline_opportunity/opp_ability.py b/pipeline_opportunity/opp_ability.py index ed3b5f6..aab7a9d 100644 --- a/pipeline_opportunity/opp_ability.py +++ b/pipeline_opportunity/opp_ability.py @@ -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, diff --git a/pipeline_opportunity/opp_data_capability.py b/pipeline_opportunity/opp_data_capability.py index d4fd873..db99d20 100644 --- a/pipeline_opportunity/opp_data_capability.py +++ b/pipeline_opportunity/opp_data_capability.py @@ -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): diff --git a/scripts/load_path.py b/scripts/load_path.py index aeb23fb..a6abdc2 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -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, ] diff --git a/wwwroot/api/opp_overseas_items.dspy b/wwwroot/api/opp_overseas_items.dspy new file mode 100644 index 0000000..a2a7861 --- /dev/null +++ b/wwwroot/api/opp_overseas_items.dspy @@ -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 + }] +} diff --git a/wwwroot/api/opp_references_popup.dspy b/wwwroot/api/opp_references_popup.dspy index 26d4d1f..00841b4 100644 --- a/wwwroot/api/opp_references_popup.dspy +++ b/wwwroot/api/opp_references_popup.dspy @@ -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 }