diff --git a/wwwroot/api/my_pipelines.dspy b/wwwroot/api/my_pipelines.dspy new file mode 100644 index 0000000..fdbdca0 --- /dev/null +++ b/wwwroot/api/my_pipelines.dspy @@ -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 注册到 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 + + 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) diff --git a/wwwroot/mobile_agent.ui b/wwwroot/mobile_agent.ui new file mode 100644 index 0000000..59ab103 --- /dev/null +++ b/wwwroot/mobile_agent.ui @@ -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": "输入你的需求..." + } + } + ] +}