feat(mobile): 未购产线门禁——mobile_agent.ui加遮罩引导层(尚未购买任何产线→去商城/用通用助手,遮罩desktop_hidden+select有产线即hide)+agent_chat.dspy服务端订阅门禁(未购/过期拒绝对话,平台角色豁免,DB异常fail-open)+my_pipelines加mode=full+agent_menus通用模式工作空间按钮+agent_generic工作空间改_generic锁专属目录(2026-09-08用户三条要求)

This commit is contained in:
yumoqing 2026-09-08 16:01:13 +08:00
parent d3c5b4f7e1
commit 2cfbc55dc3
5 changed files with 113 additions and 14 deletions

View File

@ -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"}}
]
},
{

View File

@ -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:

View File

@ -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")

View File

@ -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 注册到 ServerEnvdspy 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:

View File

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