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');"}] + } + ] } ] }