diff --git a/pipeline_core/ability.py b/pipeline_core/ability.py index eac3b89..f738324 100644 --- a/pipeline_core/ability.py +++ b/pipeline_core/ability.py @@ -50,6 +50,7 @@ class PipelineAbility: system_prompt: str = "" # 产线专属 prompt 片段(追加到通用心智后) handlers: Dict[str, Callable] = field(default_factory=dict) # {tool_name: handler} roles: List[RoleSpec] = field(default_factory=list) # 产线角色集 + menus: List[Dict] = field(default_factory=list) # 产线功能菜单(AgentIO 上方)[{"label","icon","url","type"}] # ── 全局注册表 ── @@ -122,3 +123,9 @@ def list_roles(pipeline_id: str) -> List[RoleSpec]: """列出某产线的全部角色。""" a = get_ability(pipeline_id) return a.roles if a else [] + + +def get_ability_menus(pipeline_id: str) -> List[Dict]: + """取某产线的功能菜单(AgentIO 上方,菜单/卡片/按钮)。""" + a = get_ability(pipeline_id) + return a.menus if a else [] diff --git a/pipeline_core/skill_loader.py b/pipeline_core/skill_loader.py index 01cf4d8..b39e771 100644 --- a/pipeline_core/skill_loader.py +++ b/pipeline_core/skill_loader.py @@ -277,6 +277,29 @@ class SkillLoader: return merged + def get_skill_catalog(self, pipeline_id: str = "", role: str = "", + project_id: str = "", org_id: str = "", + user_id: str = "") -> List[tuple]: + """返回可用技能目录 [(name, description)],供 LLM 语义检索(技能检索必须 LLM 做)。""" + merged = self.get_merged(pipeline_id, role, project_id, org_id, user_id) + return [(s.name, (s.description or "")[:150]) for s in sorted( + merged.values(), key=lambda s: (SCOPE_PRIORITY.get(s.scope, 9), s.name))] + + def build_prompt_block_by_names(self, names: List[str], + pipeline_id: str = "", role: str = "", + project_id: str = "", org_id: str = "", + user_id: str = "") -> str: + """按技能名构建目录层(LLM 检索结果 → 目录层)。""" + merged = self.get_merged(pipeline_id, role, project_id, org_id, user_id) + skills = [merged[n] for n in names if n in merged] + if not skills: + return "" + blocks = ["## 可用技能\n"] + for s in skills: + blocks.append(f"- **[{SCOPE_TAG.get(s.scope, s.scope)}] {s.name}**: {s.description[:200]}") + blocks.append("") + return "\n".join(blocks) + def get_by_trigger(self, user_input: str, pipeline_id: str = "", role: str = "", project_id: str = "", org_id: str = "", user_id: str = "", diff --git a/wwwroot/agent/index.ui b/wwwroot/agent/index.ui index ae2a57b..6f06bb0 100644 --- a/wwwroot/agent/index.ui +++ b/wwwroot/agent/index.ui @@ -11,6 +11,11 @@ {"widgettype": "Filler"} ] }, + { + "widgettype": "urlwidget", + "id": "ability_menu", + "options": {"url": "{{entire_url('/pipeline_core/api/agent_menus.dspy')}}", "method": "GET"} + }, { "widgettype": "AgentIO", "id": "chat_io", diff --git a/wwwroot/api/agent_menus.dspy b/wwwroot/api/agent_menus.dspy new file mode 100644 index 0000000..caaa59a --- /dev/null +++ b/wwwroot/api/agent_menus.dspy @@ -0,0 +1,69 @@ +# agent_menus.dspy - 返回当前产线的功能菜单(AgentIO 上方菜单/按钮) +# 由 core 通用 agent 的 index.ui 通过 urlwidget 加载,动态渲染产线固化的联机功能入口 + +import json as _json + +uid = await get_user() +if not uid: + uid = 'user-01' + +# 1. 解析当前产线:用户当前项目 → sd_projects.pipeline_id +pipeline_id = '' +try: + dbname = get_module_dbname('pipeline_core') + async with DBPools().sqlorContext(dbname) as sor: + recs = await sor.sqlExe( + "SELECT current_project_id FROM pipeline_agent_settings WHERE user_id=${u}$", + {"u": uid}) + pid = getattr(recs[0], 'current_project_id', '') if recs else '' + 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: + 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. 构建菜单按钮(点击 → Popup 打开功能 .ui) +buttons = [] +for i, m in enumerate(menus): + label = str(m.get("label", "") or "") + url = str(m.get("url", "") or "") + if not url: + continue + script = ( + "var pw=new bricks.PopupWindow({title:" + _json.dumps(label) + + ",cwidth:62,cheight:32,auto_open:true});" + "pw.content_w.add_widget(new bricks.urlwidget({url:" + _json.dumps(url) + "}));" + ) + buttons.append({ + "widgettype": "Button", + "options": {"label": label, "css": "small", "id": "menu_btn_%d" % i}, + "binds": [{ + "wid": "self", "event": "click", "actiontype": "script", + "target": "self", "script": script, + }], + }) + +if not buttons: + return _json.dumps({"widgettype": "HBox", "options": {"cheight": 0}, "subwidgets": []}) + +return _json.dumps({ + "widgettype": "HBox", + "options": {"width": "100%", "padding": "0 24px 8px 24px", "gap": "8px", "alignItems": "center"}, + "subwidgets": buttons, +}, ensure_ascii=False)