From 166e282d26b210beb3991dbc4b580a5f4a295351 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Fri, 11 Sep 2026 18:29:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E4=BC=9A=E8=AF=9D=E3=80=8C?= =?UTF-8?q?=F0=9F=93=81=20=E9=A1=B9=E7=9B=AE=E3=80=8D=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E2=80=94=E2=80=94agent=5Fproject=5Fpopup.dsp?= =?UTF-8?q?y(menu/new/switch/create=E5=9B=9Bop);=E6=96=B0=E5=BB=BA=3D?= =?UTF-8?q?=E8=BE=93=E9=A1=B9=E7=9B=AE=E5=90=8D=E2=86=92create=5Fproject?= =?UTF-8?q?=E8=83=BD=E5=8A=9B=E5=87=BD=E6=95=B0(alloc=E7=9B=AE=E5=BD=95+?= =?UTF-8?q?=E5=AE=A1=E8=AE=A1)=E2=86=92start=E6=BF=80=E6=B4=BB+=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E8=BF=AD=E4=BB=A3=E2=86=92=E4=BC=9A=E8=AF=9D=E8=BF=9B?= =?UTF-8?q?=E5=85=A5;=E5=88=87=E6=8D=A2=3D=E6=9C=AC=E4=BA=A7=E7=BA=BF+?= =?UTF-8?q?=E6=9C=AC=E4=BA=BA=E5=88=9B=E5=BB=BA=E5=88=97=E8=A1=A8=E7=82=B9?= =?UTF-8?q?=E9=80=89=E2=86=92=E5=BD=92=E5=B1=9E=E6=A0=A1=E9=AA=8C=E2=86=92?= =?UTF-8?q?=E5=8F=8C=E5=86=99session+=E5=85=A8=E5=B1=80=E6=8C=87=E9=92=88?= =?UTF-8?q?=E2=86=92=E4=BC=9A=E8=AF=9D=E8=BF=9B=E5=85=A5;agent=5Fmenus?= =?UTF-8?q?=E7=A1=AC=E7=BC=96=E7=A0=81=E9=A1=B9=E7=9B=AE=E6=8C=89=E9=92=AE?= =?UTF-8?q?(=E6=89=80=E6=9C=89=E4=B8=93=E4=B8=9A=E4=BA=A7=E7=BA=BF?= =?UTF-8?q?=E9=80=9A=E7=94=A8);=E6=97=A0=E9=A1=B9=E7=9B=AE=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E5=8D=87=E7=BA=A7=E4=B8=BA'=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E6=88=96=E6=96=B0=E5=BB=BA'=E5=B8=A6=E7=9B=B4=E8=BE=BE?= =?UTF-8?q?=E6=8C=89=E9=92=AE(agent=5Fmenu=5Fopen/workspace=5Fpopup?= =?UTF-8?q?=E5=90=8C=E5=8F=A3=E5=BE=84);menu=5Fopen=E9=80=8F=E4=BC=A0sessi?= =?UTF-8?q?on=5Fid/pipeline=5Fid=E7=BB=99=E5=86=85=E5=B5=8C=E9=A1=B5?= =?UTF-8?q?=E4=BF=AE=E5=A4=9Atab=E4=B8=B2=E9=A1=B9=E7=9B=AE;=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E9=85=8D=E7=BD=AE=E4=B8=89=E4=BB=B6=E5=A5=97=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=E4=BC=9A=E8=AF=9D=E7=BA=A7=E8=A7=A3=E6=9E=90get=5Fses?= =?UTF-8?q?sion=5Fproject=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wwwroot/api/agent_menu_open.dspy | 56 ++- wwwroot/api/agent_menus.dspy | 12 + wwwroot/api/agent_project_popup.dspy | 333 ++++++++++++++++++ .../add_sd_project_role_models.dspy | 19 +- .../get_sd_project_role_models.dspy | 20 +- .../update_sd_project_role_models.dspy | 19 +- 6 files changed, 425 insertions(+), 34 deletions(-) create mode 100644 wwwroot/api/agent_project_popup.dspy diff --git a/wwwroot/api/agent_menu_open.dspy b/wwwroot/api/agent_menu_open.dspy index 6126ddf..fb3b453 100644 --- a/wwwroot/api/agent_menu_open.dspy +++ b/wwwroot/api/agent_menu_open.dspy @@ -32,29 +32,63 @@ if require_project: 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}) - proj_pl = getattr(precs[0], 'pipeline_id', '') or '' if precs else '' - _has_project = (not pipeline_id) or (proj_pl == pipeline_id) + _pid = await get_session_project_id(sor, uid, session_id, pipeline_id) or '' + _has_project = bool(_pid) except Exception: _has_project = True # 解析异常保守放行(不因校验故障误伤功能) if not _has_project: + # 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_base = entire_url("/pipeline_core/api/agent_project_popup.dspy") + + def _pbtn(label, css, op): + _js = ("var r=await fetch(" + json.dumps(_proj_base + "?op=" + op + + (("&" + "&".join(_proj_q)) if _proj_q else "")) + ");" + "var d=await r.json();if(d){bricks.widgetBuild(d,bricks.app);}") + return {"widgettype": "Button", "options": {"label": label, "css": css}, + "binds": [{"wid": "self", "event": "click", "actiontype": "script", + "target": "self", "script": _js}]} + return json.dumps({ "widgettype": "PopupWindow", - "options": {"title": "提示", "cwidth": 30, "cheight": 8, "auto_open": True}, + "id": "agent_menu_no_project_pw", + "options": {"title": "提示", "cwidth": 38, "cheight": 10, "auto_open": True}, "subwidgets": [{ - "widgettype": "Text", - "options": {"text": "请先在会话中切换项目", "cfontsize": 1, "color": "#64748b", "padding": "20px"} + "widgettype": "VBox", + "options": {"width": "100%", "gap": "12px", "padding": "18px 22px"}, + "subwidgets": [ + {"widgettype": "Text", + "options": {"text": "当前会话还没有项目,请先选择或新建项目", + "cfontsize": 1, "color": "#64748b"}}, + {"widgettype": "HBox", + "options": {"width": "100%", "gap": "10px"}, + "subwidgets": [_pbtn("✨ 新建项目", "primary", "new"), + _pbtn("🔀 切换项目", "small", "switch")]} + ] }] }, ensure_ascii=False) +# 把 session_id/pipeline_id 透传给内嵌页面(多 tab 会话级项目隔离): +# 内嵌 index.ui 的 data_params 会把请求参数转发给 get_*.dspy,注入的项目过滤 +# 代码据此按会话解析当前项目;不透传会退回全局指针(串 tab)。 +# ⚠️ 查询串拼在 entire_url() 之后(workspace_popup 同款写法),不把 query 喂给 entire_url。 +_inner_url = entire_url(url) +_fwd = [] +if session_id: + _fwd.append("session_id=" + session_id) +if pipeline_id: + _fwd.append("pipeline_id=" + pipeline_id) +if _fwd: + _inner_url += ("&" if "?" in _inner_url else "?") + "&".join(_fwd) + return json.dumps({ "widgettype": "PopupWindow", "options": {"title": title, "width": width, "height": height, "auto_open": True}, "subwidgets": [ - {"widgettype": "urlwidget", "options": {"url": entire_url(url)}} + {"widgettype": "urlwidget", "options": {"url": _inner_url}} ] }, ensure_ascii=False) diff --git a/wwwroot/api/agent_menus.dspy b/wwwroot/api/agent_menus.dspy index 988443a..31b2c49 100644 --- a/wwwroot/api/agent_menus.dspy +++ b/wwwroot/api/agent_menus.dspy @@ -80,6 +80,18 @@ if pipeline_id == '_generic': + "?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") diff --git a/wwwroot/api/agent_project_popup.dspy b/wwwroot/api/agent_project_popup.dspy new file mode 100644 index 0000000..6e2958a --- /dev/null +++ b/wwwroot/api/agent_project_popup.dspy @@ -0,0 +1,333 @@ +# agent_project_popup.dspy - 会话「📁 项目」统一入口弹窗(2026-09-11 用户要求) +# GET op=menu → 小弹窗:「新建项目」「切换项目」两个按钮 +# GET op=new → 新建项目弹窗:输入项目名 → 创建成功后关闭,会话进入新项目 +# GET op=switch → 项目切换弹窗:列出本产线全部项目 → 选择后关闭,会话进入该项目 +# POST op=create → 创建项目(走 project_capability.create_project,带 alloc_project_dir+审计, +# 禁止裸 INSERT)+ start_project + 初始迭代 + 写会话项目指针 +# POST op=switch → 校验项目归属(产线/机构)后写会话项目指针 +# 参数:session_id(会话级隔离,多 tab 各自项目) pipeline_id(产线隔离) +# 会话「进入项目」= 写 pipeline_session_settings + pipeline_agent_settings(与 +# AgentExecutor._persist_project 同口径);gateway/菜单/工作空间每次请求实时解析,写完即生效。 + +import json +import re as _re + +_op = (params_kw or {}).get('op', 'menu') or 'menu' +session_id = (params_kw or {}).get('session_id', '') or '' +pipeline_id = (params_kw or {}).get('pipeline_id', '') or '' + +uid = await get_user() +dbname = get_module_dbname('pipeline_core') + +_STATUS_TXT = { + 'draft': ('草稿', '#94a3b8'), + 'active': ('进行中', '#16a34a'), + 'in_progress': ('进行中', '#16a34a'), + 'paused': ('已暂停', '#d97706'), + 'completed': ('已完成', '#2563eb'), + 'archived': ('已归档', '#64748b'), +} + + +# ══════════════════ POST:创建 / 切换 ══════════════════ + +if _op in ('create', 'switch'): + if not uid: + return json.dumps({"success": False, "error": "请先登录"}, ensure_ascii=False) + if not pipeline_id or pipeline_id == '_generic': + return json.dumps({"success": False, "error": "当前会话不支持项目管理"}, ensure_ascii=False) + userorg = (await get_userorgid()) or '0' + + from appPublic.uniqueID import getID as _getID + + async def _enter_project(sor, pid): + """会话进入项目:会话级 + 全局兜底双写,迭代上下文清空(_persist_project 同口径)。""" + await sor.sqlExe( + "UPDATE pipeline_agent_settings SET current_project_id=${pid}$, current_iteration_id='' " + "WHERE user_id=${uid}$", {"pid": pid, "uid": uid}) + _ex = await sor.sqlExe( + "SELECT 1 FROM pipeline_agent_settings WHERE user_id=${uid}$", {"uid": uid}) + if not _ex: + await sor.C('pipeline_agent_settings', { + 'id': _getID(), 'user_id': uid, + 'current_project_id': pid, 'current_iteration_id': ''}) + if session_id: + await sor.sqlExe( + "UPDATE pipeline_session_settings SET current_project_id=${pid}$, current_iteration_id='' " + "WHERE user_id=${uid}$ AND session_id=${sid}$", + {"pid": pid, "uid": uid, "sid": session_id}) + _sex = await sor.sqlExe( + "SELECT 1 FROM pipeline_session_settings WHERE user_id=${uid}$ AND session_id=${sid}$", + {"uid": uid, "sid": session_id}) + if not _sex: + await sor.C('pipeline_session_settings', { + 'id': _getID(), 'user_id': uid, 'session_id': session_id, + 'current_project_id': pid, 'current_iteration_id': ''}) + await sor.sqlExe("COMMIT", {}) + + if _op == 'create': + name = ((params_kw or {}).get('name', '') or '').strip() + if not name: + return json.dumps({"success": False, "error": "请输入项目名"}, ensure_ascii=False) + if len(name) > 64: + return json.dumps({"success": False, "error": "项目名过长(最多 64 字符)"}, ensure_ascii=False) + # 项目名会作为工作空间目录名(alloc_project_dir):拒绝路径穿越与非法字符 + if ('/' in name) or ('\\' in name) or ('..' in name) or name.startswith('.') or \ + (not _re.match(r'^[\w\u4e00-\u9fff .\-()+()]+$', name)): + return json.dumps({"success": False, + "error": "项目名含非法字符(仅允许中文、字母、数字、空格 . - _ ( ) +)"}, + ensure_ascii=False) + + # 同产线重名预检:提示走切换,不静默建重名项目 + async with DBPools().sqlorContext(dbname) as sor: + _dup = await sor.sqlExe( + "SELECT id FROM sd_projects WHERE name=${n}$ AND pipeline_id=${pl}$ LIMIT 1", + {"n": name, "pl": pipeline_id}) + await sor.sqlExe("COMMIT", {}) + if _dup: + return json.dumps({"success": False, + "error": "该产线已存在同名项目,请用「切换项目」直接进入"}, + ensure_ascii=False) + + # 铁律:创建项目走能力函数(alloc_project_dir 定死目录 + record_audit 审计) + from pipeline_service.project_capability import create_project, start_project + ok, pid = await create_project( + name, project_type='web_app', description='', + org_id=userorg, created_by=uid, who=uid, + pipeline_id=pipeline_id) + if not ok: + return json.dumps({"success": False, "error": str(pid)}, ensure_ascii=False) + # draft → active(CAS+审计),与会话 agent 创建的项目状态一致 + await start_project(pid, who=uid) + + # 初始迭代(与 agent_loop_v2._t_create_project 行为一致; + # sd_iterations 无 goal 列,范围写 scope;iteration_type 用 'default') + async with DBPools().sqlorContext(dbname) as sor: + await sor.C('sd_iterations', { + 'id': _getID(), 'project_id': pid, + 'iteration_name': name + '-初始迭代', + 'iteration_type': 'default', 'status': 'in_progress', + 'priority': 1, 'seq_no': 1}) + await _enter_project(sor, pid) + return json.dumps({"success": True, "project_id": pid, "name": name}, ensure_ascii=False) + + else: # switch + pid = (params_kw or {}).get('project_id', '') or '' + if not pid: + return json.dumps({"success": False, "error": "缺少 project_id"}, ensure_ascii=False) + async with DBPools().sqlorContext(dbname) as sor: + recs = await sor.sqlExe( + "SELECT id, name, org_id, pipeline_id, created_by FROM sd_projects " + "WHERE id=${p}$ LIMIT 1", {"p": pid}) + await sor.sqlExe("COMMIT", {}) + if not recs: + return json.dumps({"success": False, "error": "项目不存在"}, ensure_ascii=False) + r0 = recs[0] + p_pl = getattr(r0, 'pipeline_id', '') or '' + p_org = getattr(r0, 'org_id', '') or '' + p_by = getattr(r0, 'created_by', '') or '' + # 产线隔离:跨产线项目不得切入本会话(与 get_session_project_id 语义一致) + if p_pl and p_pl != pipeline_id: + return json.dumps({"success": False, + "error": "该项目属于其他产线,不能切入当前会话"}, + ensure_ascii=False) + # 可见范围(2026-09-11 用户拍板):本产线 + 本人创建 + if p_by != uid: + return json.dumps({"success": False, + "error": "无权切换到该项目(仅限本人创建的项目)"}, + ensure_ascii=False) + await _enter_project(sor, pid) + return json.dumps({"success": True, "project_id": pid, + "name": getattr(r0, 'name', '') or pid}, ensure_ascii=False) + + +# ══════════════════ GET:弹窗 widget ══════════════════ + +if not uid: + return json.dumps({ + "widgettype": "PopupWindow", + "id": "agent_project_menu_pw", + "options": {"title": "📁 项目", "cwidth": 30, "cheight": 8, "auto_open": True}, + "subwidgets": [{"widgettype": "Text", + "options": {"text": "请先登录", "cfontsize": 1, "color": "#64748b", + "padding": "20px"}}] + }, ensure_ascii=False) + +_self_url = entire_url("/pipeline_core/api/agent_project_popup.dspy") +_qb = [] +if session_id: + _qb.append("session_id=" + session_id) +if pipeline_id: + _qb.append("pipeline_id=" + pipeline_id) +_qs_base = ("&" + "&".join(_qb)) if _qb else "" + + +def _destroy_js(pw_id): + return ("var _pw=bricks.getWidgetById(" + json.dumps(pw_id) + ",bricks.app);" + "if(_pw){_pw.destroy();}") + + +def _open_get_js(op): + """fetch 本端点 GET op=xxx → widgetBuild 弹出(PopupWindow 挂 body,从 bricks.app 搜)。""" + return ("var r=await fetch(" + json.dumps(_self_url + "?op=" + op + _qs_base) + ");" + "var d=await r.json();if(d){bricks.widgetBuild(d,bricks.app);}") + + +def _post_js(fields): + js = "var body=new URLSearchParams();" + for k, v in fields: + js += "body.append(" + json.dumps(k) + "," + v + ");" + js += ("var rp=await fetch(" + json.dumps(_self_url) + ",{method:'POST'," + "headers:{'Content-Type':'application/x-www-form-urlencoded'},body:body});" + "var d=await rp.json();") + return js + + +def _btn(label, css, script): + return {"widgettype": "Button", "options": {"label": label, "css": css}, + "binds": [{"wid": "self", "event": "click", "actiontype": "script", + "target": "self", "script": script}]} + + +# ── op=menu:新建 / 切换 两按钮 ── +if _op == 'menu': + return json.dumps({ + "widgettype": "PopupWindow", + "id": "agent_project_menu_pw", + "options": {"title": "📁 项目", "cwidth": 36, "cheight": 11, "auto_open": True}, + "subwidgets": [{ + "widgettype": "VBox", + "options": {"width": "100%", "gap": "12px", "padding": "18px 22px"}, + "subwidgets": [ + {"widgettype": "Text", + "options": {"text": "新建一个项目,或把当前会话切换到已有项目", + "cfontsize": 0.85, "color": "#64748b"}}, + {"widgettype": "HBox", + "options": {"width": "100%", "gap": "10px"}, + "subwidgets": [ + _btn("✨ 新建项目", "primary", + _destroy_js("agent_project_menu_pw") + _open_get_js("new")), + _btn("🔀 切换项目", "small", + _destroy_js("agent_project_menu_pw") + _open_get_js("switch")), + ]} + ] + }] + }, ensure_ascii=False) + +# ── op=new:输入项目名 ── +if _op == 'new': + _create_js = ( + "var cw=bricks.getWidgetById('proj_new_name',bricks.app);var cv='';" + "if(cw){cv=(typeof cw.resultValue==='function')?cw.resultValue():" + "((cw.dom_element&&cw.dom_element.value)||'');}" + "cv=(cv===null||cv===undefined)?'':String(cv).trim();" + "if(!cv){var mn=new bricks.Message({title:'提示',message:'请输入项目名'});mn.open();return;}" + + _post_js([('op', "'create'"), ('name', 'cv'), + ('session_id', json.dumps(session_id)), + ('pipeline_id', json.dumps(pipeline_id))]) + + "if(d.success){" + + _destroy_js("agent_project_new_pw") + + "var mo=new bricks.Message({title:'已创建',message:'项目「'+d.name+'」已创建,会话已进入该项目'});mo.open();" + "}else{var mf=new bricks.Message({title:'创建失败',message:d.error||'未知错误'});mf.open();}") + return json.dumps({ + "widgettype": "PopupWindow", + "id": "agent_project_new_pw", + "options": {"title": "✨ 新建项目", "cwidth": 40, "cheight": 13, "auto_open": True}, + "subwidgets": [{ + "widgettype": "VBox", + "options": {"width": "100%", "gap": "10px", "padding": "16px 20px"}, + "subwidgets": [ + {"widgettype": "Text", + "options": {"text": "项目名(将同时作为项目工作空间目录名)", + "cfontsize": 0.8, "color": "#64748b"}}, + {"widgettype": "UiStr", "id": "proj_new_name", + "options": {"name": "proj_new_name", "placeholder": "输入项目名", + "width": "100%", "cfontsize": 0.9}}, + {"widgettype": "HBox", + "options": {"width": "100%", "gap": "10px", "halign": "right"}, + "subwidgets": [ + _btn("创建", "primary", _create_js), + _btn("取消", "small", _destroy_js("agent_project_new_pw")), + ]} + ] + }] + }, ensure_ascii=False) + +# ── op=switch:本产线项目列表 ── +projects = [] +cur_pid = '' +try: + from pipeline_service.workspace import get_session_project_id + async with DBPools().sqlorContext(dbname) as sor: + cur_pid = await get_session_project_id(sor, uid, session_id, pipeline_id) or '' + recs = await sor.sqlExe( + "SELECT id, name, status, created_at FROM sd_projects " + "WHERE pipeline_id=${pl}$ AND created_by=${uid}$ " + "ORDER BY created_at DESC LIMIT 100", + {"pl": pipeline_id, "uid": uid}) + await sor.sqlExe("COMMIT", {}) + projects = [dict(r) for r in (recs or [])] +except Exception as e: + debug('agent_project_popup switch list error: ' + str(e)) + projects = [] + +rows = [] +if not projects: + rows.append({"widgettype": "Text", + "options": {"text": "本产线暂无项目,请先「新建项目」", + "cfontsize": 0.9, "color": "#64748b", "padding": "16px"}}) +for p in projects: + _pid_v = str(p.get('id') or '') + _pname = str(p.get('name') or _pid_v) + _st = str(p.get('status') or '') + _st_txt, _st_color = _STATUS_TXT.get(_st, (_st or '—', '#64748b')) + _is_cur = (_pid_v == cur_pid) + _switch_js = ( + _post_js([('op', "'switch'"), ('project_id', json.dumps(_pid_v)), + ('session_id', json.dumps(session_id)), + ('pipeline_id', json.dumps(pipeline_id))]) + + "if(d.success){" + + _destroy_js("agent_project_switch_pw") + + "var mo=new bricks.Message({title:'已切换',message:'会话已进入项目「'+d.name+'」'});mo.open();" + "}else{var mf=new bricks.Message({title:'切换失败',message:d.error||'未知错误'});mf.open();}") + rows.append({ + "widgettype": "HBox", + "options": {"width": "100%", "gap": "10px", "alignItems": "center", + "padding": "10px 12px", "borderRadius": "6px", + "bgcolor": "#f0fdf4" if _is_cur else "#ffffff", + "border": "1px solid #e2e8f0", + "style": {"cursor": "pointer"}}, + "subwidgets": [ + {"widgettype": "Text", + "options": {"text": ("✓ " if _is_cur else "") + _pname, + "cfontsize": 0.95, "color": "#0f172a", "css": "filler", + "halign": "left"}}, + {"widgettype": "Text", + "options": {"text": _st_txt, "cfontsize": 0.75, "color": "#ffffff", + "bgcolor": _st_color, "padding": "2px 8px", + "borderRadius": "8px", "whiteSpace": "nowrap"}}, + {"widgettype": "Text", + "options": {"text": str(p.get('created_at') or '')[:16], + "cfontsize": 0.75, "color": "#94a3b8"}}, + ], + "binds": [{"wid": "self", "event": "click", "actiontype": "script", + "target": "self", "script": _switch_js}], + }) + +return json.dumps({ + "widgettype": "PopupWindow", + "id": "agent_project_switch_pw", + "options": {"title": "🔀 切换项目", "width": "60%", "height": "70%", "auto_open": True}, + "subwidgets": [{ + "widgettype": "VBox", + "options": {"css": "filler", "width": "100%", "height": "100%", "gap": "0px"}, + "subwidgets": [ + {"widgettype": "Text", + "options": {"text": "点击项目行即切换(仅显示本产线、本人创建的项目)", + "cfontsize": 0.8, "color": "#94a3b8", "padding": "10px 14px 4px 14px"}}, + {"widgettype": "VScrollPanel", + "options": {"css": "filler", "width": "100%", "padding": "8px 14px", "gap": "6px"}, + "subwidgets": rows} + ] + }] +}, ensure_ascii=False) diff --git a/wwwroot/sd_project_role_models/add_sd_project_role_models.dspy b/wwwroot/sd_project_role_models/add_sd_project_role_models.dspy index b2f2af1..a1b885d 100644 --- a/wwwroot/sd_project_role_models/add_sd_project_role_models.dspy +++ b/wwwroot/sd_project_role_models/add_sd_project_role_models.dspy @@ -8,17 +8,22 @@ if not id or len(id) > 32: id = uuid() ns['id'] = id -# 会话当前项目强制(auto):project_id 用当前项目,忽略前端传值 +# 会话当前项目强制过滤(2026-09-11 升级 v2):会话级解析(session→全局兜底+产线隔离+悬空自愈),无项目报错 _uid = await get_user() if not _uid: _uid = 'user-01' -_dbname = get_module_dbname('pipeline_core') -async with DBPools().sqlorContext(_dbname) as _sor: - _prec = await _sor.sqlExe("SELECT current_project_id FROM pipeline_agent_settings WHERE user_id=${u}$", {"u": _uid}) - await _sor.sqlExe("COMMIT", {}) - _cur_pid = getattr(_prec[0], 'current_project_id', '') if _prec else '' +_sid = (params_kw or {}).get('session_id', '') or '' +_pl = (params_kw or {}).get('pipeline_id', '') or 'sdlc_general' +_cur_pid = '' +try: + from pipeline_service.workspace import get_session_project_id + _dbname = get_module_dbname('pipeline_core') + async with DBPools().sqlorContext(_dbname) as _sor: + _cur_pid = await get_session_project_id(_sor, _uid, _sid, _pl) or '' +except Exception: + _cur_pid = '' if not _cur_pid: - return {"widgettype":"Error","options":{"title":"错误","timeout":3,"cwidth":20,"cheight":9,"message":"请先在会话中切换项目"}} + return {"widgettype":"Error","options":{"title":"错误","timeout":3,"cwidth":22,"cheight":9,"message":"当前会话没有项目,请先选择或新建项目"}} ns['project_id'] = _cur_pid if not ns.get('role'): diff --git a/wwwroot/sd_project_role_models/get_sd_project_role_models.dspy b/wwwroot/sd_project_role_models/get_sd_project_role_models.dspy index 03c0e1f..b59b71f 100644 --- a/wwwroot/sd_project_role_models/get_sd_project_role_models.dspy +++ b/wwwroot/sd_project_role_models/get_sd_project_role_models.dspy @@ -1,20 +1,22 @@ ns = params_kw.copy() -# 会话当前项目强制过滤(auto):读 current_project_id,无项目报错 +# 会话当前项目强制过滤(2026-09-11 升级 v2):会话级解析(session→全局兜底+产线隔离+悬空自愈),无项目报错 _uid = await get_user() if not _uid: _uid = 'user-01' +_sid = (params_kw or {}).get('session_id', '') or '' +_pl = (params_kw or {}).get('pipeline_id', '') or 'sdlc_general' _cur_pid = '' -_dbname = get_module_dbname('pipeline_core') -async with DBPools().sqlorContext(_dbname) as _sor: - _prec = await _sor.sqlExe( - "SELECT current_project_id FROM pipeline_agent_settings WHERE user_id=${u}$", - {"u": _uid}) - await _sor.sqlExe("COMMIT", {}) - _cur_pid = getattr(_prec[0], 'current_project_id', '') if _prec else '' +try: + from pipeline_service.workspace import get_session_project_id + _dbname = get_module_dbname('pipeline_core') + async with DBPools().sqlorContext(_dbname) as _sor: + _cur_pid = await get_session_project_id(_sor, _uid, _sid, _pl) or '' +except Exception: + _cur_pid = '' if not _cur_pid: - return {"widgettype":"Error","options":{"title":"错误","timeout":3,"cwidth":20,"cheight":9,"message":"请先在会话中切换项目"}} + return {"widgettype":"Error","options":{"title":"错误","timeout":3,"cwidth":22,"cheight":9,"message":"当前会话没有项目,请先选择或新建项目"}} ns['_cur_pid'] = _cur_pid userorgid = await get_userorgid() diff --git a/wwwroot/sd_project_role_models/update_sd_project_role_models.dspy b/wwwroot/sd_project_role_models/update_sd_project_role_models.dspy index ff36950..0d6c232 100644 --- a/wwwroot/sd_project_role_models/update_sd_project_role_models.dspy +++ b/wwwroot/sd_project_role_models/update_sd_project_role_models.dspy @@ -7,17 +7,22 @@ id = params_kw.id if not id: return {"widgettype":"Error","options":{"title":"Error","timeout":3,"cwidth":16,"cheight":9,"message":"需要 id"}} -# 会话当前项目强制(auto):project_id 用当前项目,忽略前端传值 +# 会话当前项目强制过滤(2026-09-11 升级 v2):会话级解析(session→全局兜底+产线隔离+悬空自愈),无项目报错 _uid = await get_user() if not _uid: _uid = 'user-01' -_dbname = get_module_dbname('pipeline_core') -async with DBPools().sqlorContext(_dbname) as _sor: - _prec = await _sor.sqlExe("SELECT current_project_id FROM pipeline_agent_settings WHERE user_id=${u}$", {"u": _uid}) - await _sor.sqlExe("COMMIT", {}) - _cur_pid = getattr(_prec[0], 'current_project_id', '') if _prec else '' +_sid = (params_kw or {}).get('session_id', '') or '' +_pl = (params_kw or {}).get('pipeline_id', '') or 'sdlc_general' +_cur_pid = '' +try: + from pipeline_service.workspace import get_session_project_id + _dbname = get_module_dbname('pipeline_core') + async with DBPools().sqlorContext(_dbname) as _sor: + _cur_pid = await get_session_project_id(_sor, _uid, _sid, _pl) or '' +except Exception: + _cur_pid = '' if not _cur_pid: - return {"widgettype":"Error","options":{"title":"错误","timeout":3,"cwidth":20,"cheight":9,"message":"请先在会话中切换项目"}} + return {"widgettype":"Error","options":{"title":"错误","timeout":3,"cwidth":22,"cheight":9,"message":"当前会话没有项目,请先选择或新建项目"}} ns['project_id'] = _cur_pid userorgid = await get_userorgid()