pipeline_core/wwwroot/api/agent_menus.dspy
yumoqing b5c7dd3f44 fix(agent_menus): 项目解析收敛到 get_session_project_id(含悬空引用防护)
不再手写 session→项目解析(与解析层语义漂移是「会话有项目、入口说没有」的根因之一)
2026-08-27 17:44:38 +08:00

102 lines
4.0 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 ''
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... 明文显示)。
buttons = []
# 无当前项目时,点击「需要项目」的按钮弹报错(不打开页面)
_no_proj_script = "var m=new bricks.Message({title:'提示',message:'请先在会话中切换项目'});m.open();"
def _bind(url, require_project):
if require_project and not pid:
return {
"wid": "self", "event": "click", "actiontype": "script", "target": "self",
"script": _no_proj_script,
}
return {
"wid": "self", "event": "click", "actiontype": "urlwidget",
"target": "self", "options": {"url": url},
}
# 固定「工作空间」按钮(通用能力,所有 agent 都有;依赖当前项目)
_ws_url = entire_url("/pipeline-sdlc/api/workspace_popup.dspy")
if session_id:
_ws_url += "?session_id=" + session_id
buttons.append({
"widgettype": "Button",
"options": {"label": "🗂 工作空间", "css": "small", "id": "menu_btn_ws"},
"binds": [_bind(_ws_url, True)],
})
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
buttons.append({
"widgettype": "Button",
"options": {"label": label, "css": "small", "id": "menu_btn_%d" % i},
"binds": [_bind(open_url, require_project)],
})
return _json.dumps({
"widgettype": "HBox",
"options": {"width": "100%", "padding": "0 24px 8px 24px", "gap": "8px", "alignItems": "center"},
"subwidgets": buttons,
}, ensure_ascii=False)