diff --git a/pipeline_opportunity/opp_ability.py b/pipeline_opportunity/opp_ability.py index aab7a9d..6881623 100644 --- a/pipeline_opportunity/opp_ability.py +++ b/pipeline_opportunity/opp_ability.py @@ -122,10 +122,14 @@ opp_query_tenders / opp_tender_detail),**不要**自己去爬网站, - 报告里的招标数量、预算、项目案例必须来自工具返回的真实数据,**禁止编造**; 查不到就如实写"数据未覆盖"。 - **所有分析必须带 references(可查证)**:给排名/统计/推荐结论时,调 - opp_category_references 或 opp_query_tenders 取分项明细,回答末尾附 References 章节—— - 列出自带的「📋 数据参考」菜单(用户可逐条点开查公告原文), - 并在正文中给出 3-5 条代表性分项的标题+公告链接(markdown 链接,外链会自动新窗口打开)。 -- 每条引用的招标信息带来源链接(爬虫平台已提供 url / source_url)。 + opp_category_references / opp_query_tenders / opp_demands / opp_overseas_references + 取分项明细,回答末尾附 References 章节—— + 列出自带的「📋 数据参考」菜单(招标/国内众包需求/海外需求三类主题都可逐条点开查原文), + 并在正文中给出 3-5 条代表性分项的标题+来源链接(markdown 链接,外链会自动新窗口打开)。 +- **凡是回复中出现单个数据项的列表(招标/需求/样例),每一项都必须带来源 URL**: + 用 markdown 链接把标题挂到该条的 url(公告原文/需求详情页)上,格式如 + 「[标题](url) | 来源:ccgp | 2026-08-30」;url 缺失时如实写「来源缺失」,禁止只贴标题。 +- 每条引用的招标/需求信息带来源链接(爬虫平台已提供 url / source_url)。 - 报告没被人工确认,不许当作可发起审批;审批没过,不许当作可立项。 ## 典型场景 diff --git a/pipeline_opportunity/opp_data_capability.py b/pipeline_opportunity/opp_data_capability.py index db99d20..bc3a6a7 100644 --- a/pipeline_opportunity/opp_data_capability.py +++ b/pipeline_opportunity/opp_data_capability.py @@ -37,13 +37,15 @@ async def hot_software(sor, days=30, top=10): {"days": days, "top": top}) -async def category_references(sor, category, days=30, limit=500): +async def category_references(sor, category, days=30, limit=500, record_type="tender"): """某软件主题的全部分项(references 可查证)。 - 每条带公告原文 url + 采集来源 source_url,供界面逐条查证。 + 每条带公告原文/详情页 url + 采集来源 source_url,供界面逐条查证。 + record_type: tender=招标(默认) / demand=众包需求发布(猪八戒/一品威客)。 """ return await crawler_get(sor, "/api/category_references", - {"category": category, "days": days, "limit": limit}) + {"category": category, "days": days, "limit": limit, + "record_type": record_type}) async def crawler_stats(sor): diff --git a/scripts/load_path.py b/scripts/load_path.py index a6abdc2..f093d5d 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_demand_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_demand_items.dspy b/wwwroot/api/opp_demand_items.dspy new file mode 100644 index 0000000..47ace65 --- /dev/null +++ b/wwwroot/api/opp_demand_items.dspy @@ -0,0 +1,89 @@ +# opp_demand_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 category_references + ok, res = await category_references(sor, category, days=days, limit=500, record_type="demand") + await sor.sqlExe("COMMIT", {}) + +SRC_NAME = {"zbj": "猪八戒", "epwk": "一品威客"} + +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 = str(it.get("source_url") 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');"}]}) + if src and src != url: + sub_btns.append({"widgettype": "Button", "options": {"label": "来源页", "css": "small"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "script", + "target": "self", + "script": "window.open(" + _json.dumps(src) + ",'_blank','noopener');"}]}) + budget = it.get("budget_wan") or 0 + plat = SRC_NAME.get(str(it.get("source") or ""), str(it.get("source") or "众包平台")) + meta = "%s | %s | %s" % ( + str(it.get("publish_time") or "")[:10], + plat, + (("预算 %.0f 万" % 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("count", len(rows)) if ok else 0 +return { + "widgettype": "PopupWindow", + "id": "opp_demand_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": "每条「需求详情」为众包平台(猪八戒/一品威客)原始需求页,「来源页」为采集来源页,均在新窗口打开,可逐条查证。", + "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 00841b4..0d3226f 100644 --- a/wwwroot/api/opp_references_popup.dspy +++ b/wwwroot/api/opp_references_popup.dspy @@ -1,5 +1,7 @@ # opp_references_popup.dspy - 数据参考(references):主题列表(agent_menu_open 弹窗内嵌内容) -# 点主题「查看分项」→ opp_references_items.dspy 列分项(每条带来源,点击新窗口查证) +# 点主题「查看分项」→ 对应分项列表(每条带来源,点击新窗口查证): +# 招标主题 → opp_references_items.dspy;国内众包需求主题 → opp_demand_items.dspy; +# 海外众包需求主题 → opp_overseas_items.dspy import json as _json @@ -10,12 +12,15 @@ if not uid: dbname = get_module_dbname('pipeline-opportunity') items_url = entire_url("/pipeline-opportunity/api/opp_references_items.dspy") +demand_items_url = entire_url("/pipeline-opportunity/api/opp_demand_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, hot_overseas + from pipeline_opportunity.opp_data_capability import hot_software, hot_demands, hot_overseas ok, res = await hot_software(sor, days=30, top=10) await sor.sqlExe("COMMIT", {}) + ok_dm, res_dm = await hot_demands(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", {}) @@ -57,6 +62,44 @@ else: ] }) +# ── 国内众包需求板块(猪八戒/一品威客)── +dm_rows = [] +if ok_dm: + dm_ranking = res_dm.get("ranking") or [] + if dm_ranking: + dm_rows.append({"widgettype": "Text", "options": { + "text": "国内众包软件需求(猪八戒/一品威客,近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 dm_ranking: + cat = str(r.get("category", "")) + cnt = r.get("tender_count", 0) + budget = r.get("total_budget_wan", 0) + script = ("var old=bricks.getWidgetById('opp_demand_items_pw',bricks.app);" + "if(old&&old.destroy){old.destroy();}" + "var rp=await fetch(" + _json.dumps(demand_items_url) + "+'?category='+encodeURIComponent(" + _json.dumps(cat) + "));" + "var d=await rp.json();" + "if(d){bricks.widgetBuild(d,bricks.app);}") + dm_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": "预算 %s 万" % ("{:,.0f}".format(budget) if budget else "—"), + "cfontsize": 0.85, "color": "#64748b"}}, + {"widgettype": "Button", "options": {"label": "查看分项", "css": "small"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "script", + "target": "self", "script": script}]} + ] + }) + # ── 海外众包需求板块(Freelancer.com / PeoplePerHour)── ov_rows = [] if ok_ov: @@ -102,7 +145,7 @@ return { "gap": "6px", "overflow": "auto"}, "subwidgets": [ {"widgettype": "Text", "options": { - "text": "点击「查看分项」列出该主题的全部招标分项;每条分项带公告原文链接,可逐条查证。", + "text": "点击「查看分项」列出该主题的全部数据项;每条数据项带来源链接(公告原文/需求详情/来源页),可逐条点开查证。", "cfontsize": 0.8, "color": "#64748b", "halign": "left", "margin": "0 0 8px 0"}} - ] + rows + ov_rows + ] + rows + dm_rows + ov_rows }