feat(mobile): 手机端产线工作界面——mobile_agent.ui(ChipBar已购产线胶囊+AgentIO全屏,mobile_opts压margin)+my_pipelines.dspy(客户按product_subscription未到期×pipelines published过滤,平台角色看全部,get_user_roles失败降级按客户安全侧)(2026-09-07手机端适配:只有购买过的产线可见)

This commit is contained in:
yumoqing 2026-09-08 14:28:37 +08:00
parent 01bfd01619
commit c7b766e1c1
2 changed files with 124 additions and 0 deletions

View File

@ -0,0 +1,72 @@
# my_pipelines.dspy - 当前用户可用产线列表 → ChipBar 数据手机端「产线」Tab 胶囊条)
# 口径2026-09-07 用户定夺):客户只显示购买过且未到期的产线;
# 平台侧owner./reseller. 角色)显示全部 published 产线。
# 数据链product_subscription(status='1', end_date>=今天) × product(product_type='pipeline',
# resource_ref_id=pipelines.id) × pipelines(status='published')。
import json as _json
uid = await get_user()
if not uid:
return _json.dumps({"items": [], "error": "未登录"}, ensure_ascii=False)
orgid = await get_userorgid()
dbname = get_module_dbname('pipeline_core')
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
async with DBPools().sqlorContext(dbname) as sor:
if is_ops:
recs = await sor.sqlExe(
"SELECT id, name FROM pipelines WHERE status='published' ORDER BY id", {})
await sor.sqlExe("COMMIT", {})
ids = [(getattr(r, 'id', ''), getattr(r, 'name', '')) for r in (recs or [])]
else:
srecs = await sor.sqlExe(
"SELECT DISTINCT pr.resource_ref_id AS rid 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'", {"org": orgid})
await sor.sqlExe("COMMIT", {})
rids = []
for r in (srecs or []):
v = getattr(r, 'rid', '')
if v and v not in rids:
rids.append(v)
ids = []
if rids:
# sqlor IN 列表必须展开占位符(传 list 会崩)
ph = {}
marks = []
for i, rid in enumerate(rids):
k = 'k%d' % i
ph[k] = rid
marks.append('${%s}$' % k)
prec = await sor.sqlExe(
"SELECT id, name FROM pipelines WHERE status='published'"
" AND id IN (" + ','.join(marks) + ") ORDER BY id", ph)
await sor.sqlExe("COMMIT", {})
ids = [(getattr(r, 'id', ''), getattr(r, 'name', '')) for r in (prec or [])]
for pid, pname in ids:
items.append({"id": pid, "pipeline_id": pid,
"name": pname or pid, "session_id": "m_" + pid})
except Exception:
items = []
return _json.dumps({"items": items}, ensure_ascii=False)

52
wwwroot/mobile_agent.ui Normal file
View File

@ -0,0 +1,52 @@
{
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "0"},
"subwidgets": [
{
"widgettype": "ChipBar",
"id": "mobile_pipeline_chips",
"options": {
"width": "100%",
"height": "auto",
"dataurl": "/pipeline_core/api/my_pipelines.dspy",
"valueField": "id",
"textField": "name",
"empty_text": "暂无已购产线 · 请到「我的 → 产品商城」购买"
},
"binds": [
{
"wid": "self",
"event": "select",
"actiontype": "method",
"target": "app.mobile_agent_io",
"method": "set_pipeline"
},
{
"wid": "self",
"event": "select",
"actiontype": "urlwidget",
"target": "app.mobile_menus_wrap",
"options": {"url": "/pipeline_core/api/agent_menus.dspy", "method": "GET"}
}
]
},
{
"widgettype": "VBox",
"id": "mobile_menus_wrap",
"options": {"width": "100%", "height": "auto"}
},
{
"widgettype": "AgentIO",
"id": "mobile_agent_io",
"options": {
"css": "filler",
"margin": "0 24px 24px 24px",
"mobile_opts": {"margin": "0 8px 8px 8px"},
"url": "/pipeline_core/api/agent_chat.dspy",
"model_dataurl": "/pipeline_core/api/agent_model_options.dspy?capabilities=chat",
"model_cwidth": 14,
"placeholder": "输入你的需求..."
}
}
]
}