From 2cfbc55dc370bc7788ee47125b320808a73a1467 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Tue, 8 Sep 2026 16:01:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(mobile):=20=E6=9C=AA=E8=B4=AD=E4=BA=A7?= =?UTF-8?q?=E7=BA=BF=E9=97=A8=E7=A6=81=E2=80=94=E2=80=94mobile=5Fagent.ui?= =?UTF-8?q?=E5=8A=A0=E9=81=AE=E7=BD=A9=E5=BC=95=E5=AF=BC=E5=B1=82(?= =?UTF-8?q?=E5=B0=9A=E6=9C=AA=E8=B4=AD=E4=B9=B0=E4=BB=BB=E4=BD=95=E4=BA=A7?= =?UTF-8?q?=E7=BA=BF=E2=86=92=E5=8E=BB=E5=95=86=E5=9F=8E/=E7=94=A8?= =?UTF-8?q?=E9=80=9A=E7=94=A8=E5=8A=A9=E6=89=8B,=E9=81=AE=E7=BD=A9desktop?= =?UTF-8?q?=5Fhidden+select=E6=9C=89=E4=BA=A7=E7=BA=BF=E5=8D=B3hide)+agent?= =?UTF-8?q?=5Fchat.dspy=E6=9C=8D=E5=8A=A1=E7=AB=AF=E8=AE=A2=E9=98=85?= =?UTF-8?q?=E9=97=A8=E7=A6=81(=E6=9C=AA=E8=B4=AD/=E8=BF=87=E6=9C=9F?= =?UTF-8?q?=E6=8B=92=E7=BB=9D=E5=AF=B9=E8=AF=9D,=E5=B9=B3=E5=8F=B0?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E8=B1=81=E5=85=8D,DB=E5=BC=82=E5=B8=B8fail-o?= =?UTF-8?q?pen)+my=5Fpipelines=E5=8A=A0mode=3Dfull+agent=5Fmenus=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E6=A8=A1=E5=BC=8F=E5=B7=A5=E4=BD=9C=E7=A9=BA=E9=97=B4?= =?UTF-8?q?=E6=8C=89=E9=92=AE+agent=5Fgeneric=E5=B7=A5=E4=BD=9C=E7=A9=BA?= =?UTF-8?q?=E9=97=B4=E6=94=B9=5Fgeneric=E9=94=81=E4=B8=93=E5=B1=9E?= =?UTF-8?q?=E7=9B=AE=E5=BD=95(2026-09-08=E7=94=A8=E6=88=B7=E4=B8=89?= =?UTF-8?q?=E6=9D=A1=E8=A6=81=E6=B1=82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wwwroot/agent_generic/index.ui | 5 ++-- wwwroot/api/agent_chat.dspy | 34 +++++++++++++++++++++++++ wwwroot/api/agent_menus.dspy | 18 ++++++++++++++ wwwroot/api/my_pipelines.dspy | 25 +++++++++++-------- wwwroot/mobile_agent.ui | 45 ++++++++++++++++++++++++++++++++-- 5 files changed, 113 insertions(+), 14 deletions(-) diff --git a/wwwroot/agent_generic/index.ui b/wwwroot/agent_generic/index.ui index 3111b3f..3919a17 100644 --- a/wwwroot/agent_generic/index.ui +++ b/wwwroot/agent_generic/index.ui @@ -18,8 +18,9 @@ { "widgettype": "Button", "options": {"label": "🗂 工作空间", "css": "small"}, - "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "self", "options": {"url": "/pipeline-sdlc/api/workspace_popup.dspy?pipeline_id=sdlc_general"}}] - } + "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "self", "options": {"url": "/pipeline-sdlc/api/workspace_popup.dspy?pipeline_id=_generic"}}] + }, + {"widgettype": "Text", "options": {"text": "工作空间为通用会话专属目录,不进入项目空间", "cfontsize": 0.75, "color": "#94a3b8"}} ] }, { diff --git a/wwwroot/api/agent_chat.dspy b/wwwroot/api/agent_chat.dspy index 2ead1c9..8d0d995 100644 --- a/wwwroot/api/agent_chat.dspy +++ b/wwwroot/api/agent_chat.dspy @@ -72,6 +72,40 @@ if action == 'send_message': # ── 产线默认能力(入口指定,如 bidding_general):无当前项目时装载该产线能力 ── pipeline_id = (params_kw or {}).get('pipeline_id', '') or '' + # ── 订阅门禁(2026-09-08 用户要求:未购买不能进入对话模式)── + # 产线对话须持有该产线有效(未到期)订阅;平台侧角色(owner./reseller.) + # 豁免;通用会话(空/_generic)豁免。桌面/手机所有入口在此统一拦截, + # 前端遮罩只做引导。DB 异常时 fail-open(可用性优先,门禁是商业限制非安全边界)。 + if pipeline_id and pipeline_id != '_generic': + _gate_ok = True + try: + _roles = await get_user_roles(uid) + _is_ops = any(str(r).startswith('owner.') or str(r).startswith('reseller.') + for r in (_roles or [])) + except Exception: + _is_ops = False + if not _is_ops: + _orgid = await get_userorgid() + try: + async with DBPools().sqlorContext(dbname) as sor: + _sub = await sor.sqlExe( + "SELECT ps.id FROM product_subscription ps" + " JOIN product pr ON ps.product_id = pr.id" + " WHERE ps.user_org_id=${org}$ AND ps.status='1'" + " AND ps.end_date >= CURDATE()" + " AND pr.product_type='pipeline'" + " AND pr.resource_ref_id=${pid}$ LIMIT 1", + {"org": _orgid, "pid": pipeline_id}) + await sor.sqlExe("COMMIT", {}) + _gate_ok = bool(_sub) + except Exception: + _gate_ok = True + if not _gate_ok: + async def _gate_stream(): + yield json.dumps({"error": "尚未购买该产线或订阅已到期,请到产品商城购买后再开始对话"}, + ensure_ascii=False) + '\n' + return await stream_response(request, _gate_stream, 'text/plain; charset=utf-8') + # ── 上传文件落盘(有项目→项目根;无项目→workspace 会话目录)+ 生成显式截断上下文 ── file_ctx = '' if _uploads: diff --git a/wwwroot/api/agent_menus.dspy b/wwwroot/api/agent_menus.dspy index e0fdf09..d9f674d 100644 --- a/wwwroot/api/agent_menus.dspy +++ b/wwwroot/api/agent_menus.dspy @@ -64,6 +64,24 @@ except Exception: buttons = [] +# 通用会话(pipeline_id=_generic):只给"工作空间"按钮,锁用户专属目录 +if pipeline_id == '_generic': + buttons.append({ + "widgettype": "Button", + "options": {"label": "🗂 工作空间", "css": "small", "id": "menu_btn_ws_generic"}, + "binds": [{ + "wid": "self", "event": "click", "actiontype": "urlwidget", + "target": "self", + "options": {"url": entire_url("/pipeline-sdlc/api/workspace_popup.dspy") + + "?pipeline_id=_generic"}, + }], + }) + return _json.dumps({ + "widgettype": "HBox", + "options": {"width": "100%", "padding": "0 12px 8px 12px", "gap": "8px", "alignItems": "center"}, + "subwidgets": buttons, + }, ensure_ascii=False) + # 固定「工作空间」按钮(通用能力,所有 agent 都有;依赖当前项目) # 带产线参数:入口产线与会话项目跨产线时视为无项目(产线隔离,弹窗端点自身在点击时判断) _ws_url = entire_url("/pipeline-sdlc/api/workspace_popup.dspy") diff --git a/wwwroot/api/my_pipelines.dspy b/wwwroot/api/my_pipelines.dspy index fdbdca0..0b5e9aa 100644 --- a/wwwroot/api/my_pipelines.dspy +++ b/wwwroot/api/my_pipelines.dspy @@ -13,21 +13,26 @@ if not uid: orgid = await get_userorgid() dbname = get_module_dbname('pipeline_core') +# mode=full(桌面侧边栏/管理场景):不过滤订阅,返回全部 published 产线; +# 默认(手机端 ChipBar):客户只见已购未到期产线(2026-09-07 用户定夺) +_mode = ((params_kw or {}).get('mode') or '').strip() + items = [] try: # 平台侧角色(owner./reseller.)看全部 published 产线;客户只看已购未到期。 # get_user_roles 由 rbac.init 注册到 ServerEnv,dspy run_ns 会带入; # 探测失败/不可用时默认按客户处理(只显示已购,安全侧,不放大可见面)。 - is_ops = False - try: - roles = await get_user_roles(uid) - for r in (roles or []): - rs = str(r) - if rs.startswith('owner.') or rs.startswith('reseller.'): - is_ops = True - break - except Exception: - is_ops = False + is_ops = _mode == 'full' + if not is_ops: + try: + roles = await get_user_roles(uid) + for r in (roles or []): + rs = str(r) + if rs.startswith('owner.') or rs.startswith('reseller.'): + is_ops = True + break + except Exception: + is_ops = False async with DBPools().sqlorContext(dbname) as sor: if is_ops: diff --git a/wwwroot/mobile_agent.ui b/wwwroot/mobile_agent.ui index 59ab103..c86f2fc 100644 --- a/wwwroot/mobile_agent.ui +++ b/wwwroot/mobile_agent.ui @@ -1,6 +1,6 @@ { "widgettype": "VBox", - "options": {"width": "100%", "height": "100%", "padding": "0"}, + "options": {"width": "100%", "height": "100%", "padding": "0", "css": "pl-gate-root"}, "subwidgets": [ { "widgettype": "ChipBar", @@ -11,7 +11,7 @@ "dataurl": "/pipeline_core/api/my_pipelines.dspy", "valueField": "id", "textField": "name", - "empty_text": "暂无已购产线 · 请到「我的 → 产品商城」购买" + "empty_text": "暂无已购产线" }, "binds": [ { @@ -27,6 +27,13 @@ "actiontype": "urlwidget", "target": "app.mobile_menus_wrap", "options": {"url": "/pipeline_core/api/agent_menus.dspy", "method": "GET"} + }, + { + "wid": "self", + "event": "select", + "actiontype": "script", + "target": "self", + "script": "var ov=bricks.getWidgetById('mobile_gate_overlay',bricks.app);if(!ov)return;var has=!!(event.params&&(event.params.id||event.params.pipeline_id));if(has){ov.hide();}else{ov.show();}" } ] }, @@ -47,6 +54,40 @@ "model_cwidth": 14, "placeholder": "输入你的需求..." } + }, + { + "widgettype": "VBox", + "id": "mobile_gate_overlay", + "options": { + "css": "pl-gate", + "width": "100%", + "height": "100%", + "bgcolor": "#0b1120", + "opacity": 0.97, + "zIndex": 500, + "alignItems": "center", + "justifyContent": "center", + "gap": "14px", + "padding": "24px", + "desktop_hidden": true + }, + "subwidgets": [ + {"widgettype": "Title3", "options": {"otext": "尚未购买任何产线", "i18n": true, "color": "#f1f5f9"}}, + {"widgettype": "Text", "options": {"otext": "购买产线后即可在此对话;通用助手无需购买。", "i18n": true, "cfontsize": 0.9, "color": "#94a3b8", "textAlign": "center", "width": "100%"}}, + { + "widgettype": "Button", + "options": {"label": "去产品商城购买", "otext": "去产品商城购买", "i18n": true, "css": "primary", "bgcolor": "#2563eb", "color": "#ffffff", "padding": "12px 28px", "borderRadius": "999px"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "PopupWindow", + "popup_options": {"title": "产品商城"}, + "options": {"url": "/product_management/storefront/index.ui"}}] + }, + { + "widgettype": "Button", + "options": {"label": "使用通用助手", "otext": "使用通用助手", "i18n": true, "css": "small", "bgcolor": "transparent", "color": "#94a3b8"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", + "script": "var tp=bricks.getWidgetById('main_content',bricks.app);if(!tp)return;tp.open_tab({name:'m_generic',label:'助手',url:'/pipeline_core/agent_generic',removable:false,refresh:false});if(tp.mobile_bar)tp.mobile_bar.set_active('m_generic');"}] + } + ] } ] }