pipeline_core/wwwroot/api/agent_menus.dspy

164 lines
7.4 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# agent_menus.dspy - 返回当前产线的功能菜单AgentIO 上方菜单/按钮)
# 由 core 通用 agent 的 index.ui 通过 urlwidget 加载,动态渲染产线固化的联机功能入口
import json as _json
uid = await get_user()
if not uid:
uid = 'user-01'
# 会话内唯一标识web 多 tab工作空间按钮按会话解析项目
session_id = (params_kw or {}).get('session_id', '') or ''
# 产线默认能力(入口指定,如 bidding_general无当前项目时用该产线菜单/能力
default_pipeline_id = (params_kw or {}).get('pipeline_id', '') or ''
# 1. 解析当前产线:当前会话项目 → sd_projects.pipeline_id
# 统一走 get_session_project_id含悬空引用防护指向已删除项目的记录自愈清理并回退全局
# 禁止在此重复实现 session→项目解析——多份实现语义漂移正是「会话有项目、入口说没有」的根因。
# 产线隔离:入口指定产线(如商机/投标)时,会话项目必须同产线才生效——
# 否则残留的跨产线项目(如开发产线的元景项目)会劫持本产线菜单/工作空间。
pipeline_id = ''
pid = ''
try:
from pipeline_service.workspace import get_session_project_id
dbname = get_module_dbname('pipeline_core')
async with DBPools().sqlorContext(dbname) as sor:
pid = await get_session_project_id(sor, uid, session_id) or ''
if pid:
precs = await sor.sqlExe(
"SELECT pipeline_id FROM sd_projects WHERE id=${p}$", {"p": pid})
if precs:
pipeline_id = getattr(precs[0], 'pipeline_id', '') or ''
if default_pipeline_id and pipeline_id != default_pipeline_id:
# 跨产线项目劫持:清空项目,用入口产线
pid = ''
pipeline_id = default_pipeline_id
except Exception:
pass
if not pipeline_id:
if default_pipeline_id:
pipeline_id = default_pipeline_id
else:
try:
from pipeline_core import DEFAULT_ABILITY_ID
pipeline_id = DEFAULT_ABILITY_ID
except Exception:
pipeline_id = 'sdlc_general'
# 2. 读产线功能菜单
try:
from pipeline_core.ability import get_ability_menus
menus = get_ability_menus(pipeline_id)
except Exception:
menus = []
# 3. 构建功能入口actiontype=urlwidget 打开通用弹窗 agent_menu_open控制弹窗大小
# 注意:这里不预先 urlencode 查询参数。前端 bricks_fetch 的 url_parse 会把 url 里的
# 查询参数提取后再 encodeURIComponent 编码一次,若此处已 urlencode 会导致双重编码
# title 的 emoji/中文会变成 %F0%9F... 明文显示)。
#
# 「需要项目」判断在【点击时】由 agent_menu_open 服务端做(传 require_project/session_id/pipeline_id
# 渲染时固化判断是老 bug——页面加载时还没切项目 → 按钮写死报错脚本,之后切了项目仍报错。
#
# 端形态2026-09-08 用户要求:产线上方的按钮收缩为一个菜单项,给会话留最大空间):
# 桌面:一排 Button原样
# 手机jsoncall 自动带 _is_mobile=1单个「⋯ 功能」Button → 点击弹
# MobileSheet 竖向菜单,项点击走同一 open_urlpopup:trueagent_menu_open
# 返回 PopupWindow 自开,与桌面链路完全一致)
_is_mobile = str((params_kw or {}).get('_is_mobile', '') or '') == '1'
actions = [] # [{label, url}] 统一收集,最后按端形态渲染
# 通用会话pipeline_id=_generic只给"工作空间"入口,锁用户专属目录
if pipeline_id == '_generic':
actions.append({
"label": "🗂 工作空间",
"url": entire_url("/pipeline-sdlc/api/workspace_popup.dspy")
+ "?pipeline_id=_generic",
})
else:
# 固定「📁 项目」入口2026-09-11 用户要求,所有专业产线通用):
# 点击弹出「新建项目 / 切换项目」;新建输项目名后关闭、会话进入新项目;
# 切换列出本产线全部项目、选择后关闭、会话进入所选项目。
_proj_q = []
if session_id:
_proj_q.append("session_id=" + session_id)
if pipeline_id:
_proj_q.append("pipeline_id=" + pipeline_id)
_proj_url = (entire_url("/pipeline_core/api/agent_project_popup.dspy") + "?op=menu"
+ (("&" + "&".join(_proj_q)) if _proj_q else ""))
actions.append({"label": "📁 项目", "url": _proj_url})
# 固定「工作空间」入口(通用能力,所有 agent 都有;依赖当前项目)
# 带产线参数:入口产线与会话项目跨产线时视为无项目(产线隔离,弹窗端点自身在点击时判断)
_ws_url = entire_url("/pipeline-sdlc/api/workspace_popup.dspy")
_ws_q = []
if session_id:
_ws_q.append("session_id=" + session_id)
if pipeline_id:
_ws_q.append("pipeline_id=" + pipeline_id)
if _ws_q:
_ws_url += "?" + "&".join(_ws_q)
actions.append({"label": "🗂 工作空间", "url": _ws_url})
for i, m in enumerate(menus):
label = str(m.get("label", "") or "")
url = str(m.get("url", "") or "")
if not url:
continue
require_project = bool(m.get("require_project", False))
width = str(m.get("width", "85%") or "85%")
height = str(m.get("height", "80%") or "80%")
open_url = (entire_url("/pipeline_core/api/agent_menu_open.dspy")
+ "?url=" + url + "&title=" + label
+ "&width=" + width + "&height=" + height
+ ("&require_project=1" if require_project else ""))
if session_id:
open_url += "&session_id=" + session_id
if pipeline_id:
open_url += "&pipeline_id=" + pipeline_id
actions.append({"label": label, "url": open_url})
if _is_mobile:
# 手机端:收缩为单个「⋯ 功能」按钮 → MobileSheet 竖向菜单。
# items 内联进脚本JSON 字面量popup:true → 点击 widgetBuild urlwidget
# 服务端返回 PopupWindow 自开agent_menu_open / workspace_popup 同链路)。
_items = [{"name": "act_%d" % i, "label": a["label"], "url": a["url"], "popup": True}
for i, a in enumerate(actions)]
_script = (
"var its=" + _json.dumps(_items, ensure_ascii=False) + ";"
"var sh=new bricks.MobileSheet({title:'功能',items:its});sh.open();"
)
return _json.dumps({
"widgettype": "HBox",
"options": {"width": "100%", "padding": "0 12px 6px 12px", "gap": "8px", "alignItems": "center"},
"subwidgets": [{
"widgettype": "Button",
"options": {"label": "⋯ 功能", "css": "small", "id": "menu_btn_mobile_more"},
"binds": [{
"wid": "self", "event": "click", "actiontype": "script",
"target": "self", "script": _script,
}],
}],
}, ensure_ascii=False)
buttons = []
for i, a in enumerate(actions):
buttons.append({
"widgettype": "Button",
"options": {"label": a["label"], "css": "small", "id": "menu_btn_%d" % i},
"binds": [{
"wid": "self", "event": "click", "actiontype": "urlwidget",
"target": "self", "options": {"url": a["url"]},
}],
})
return _json.dumps({
"widgettype": "HBox",
"options": {"width": "100%", "padding": "0 24px 8px 24px", "gap": "8px", "alignItems": "center"},
"subwidgets": buttons,
}, ensure_ascii=False)