diff --git a/pipeline_service/agent_loop_v2.py b/pipeline_service/agent_loop_v2.py index 1a04f26..770e525 100644 --- a/pipeline_service/agent_loop_v2.py +++ b/pipeline_service/agent_loop_v2.py @@ -26,7 +26,7 @@ from typing import AsyncGenerator, Dict, List, Optional logger = logging.getLogger("pipeline.agent_executor") -from .workspace import WORKSPACE_BASE +from .workspace import WORKSPACE_BASE, GENERAL_SPACE # ── 默认工具定义(在 pipeline-core 未加载时使用)── @@ -63,6 +63,7 @@ class AgentExecutor: model_name: str = None, role: str = "", # 产线内角色(可选,如 develop/design;驾驶舱 agent 为空) base_url: str = "", # 请求 base_url(scheme://host/),供 slash 命令生成 widget 绝对 URL + generic: bool = False, # True = 纯通用会话(不解析项目、不挂产线能力) ): self.config = config self.project_id = project_id @@ -70,6 +71,8 @@ class AgentExecutor: self.org_id = "" # loaded from project context self.pipeline_id = "" # loaded from project context(产线能力包 key) self.role = role # 产线内角色(技能/工具/prompt 按角色加载) + self.generic = generic # 是否纯通用会话 + self.space = GENERAL_SPACE # 项目空间键:generic→'general',产线→真实 pipeline_id self.workspace_dir = workspace_dir or WORKSPACE_BASE self.model_name = model_name or config.model_name self.base_url = base_url @@ -362,6 +365,9 @@ class AgentExecutor: except ImportError: pass + # 6. 项目空间键:generic→'general'(通用助手),否则用真实 pipeline_id(产线之间隔离) + self.space = GENERAL_SPACE if self.generic else (self.pipeline_id or GENERAL_SPACE) + async def _resolve_skills_base_dir(self): """技能根目录 = 机构工作目录/skills/(多租户隔离,不在全局应用目录)。""" try: @@ -755,9 +761,10 @@ class AgentExecutor: if not name: return "需要项目名称" - # 精确匹配 + # 精确匹配(限定当前项目空间 + 机构) recs = await sor.sqlExe( - "SELECT id, name FROM sd_projects WHERE name=${n}$", {"n": name}) + "SELECT id, name FROM sd_projects WHERE name=${n}$ AND pipeline_id=${s}$", + {"n": name, "s": self.space}) if recs: r = recs[0] self.project_id = getattr(r, "id", "") @@ -769,14 +776,16 @@ class AgentExecutor: from pipeline_service.llm_bridge import llm_call all_recs = await sor.sqlExe( - "SELECT name FROM sd_projects ORDER BY created_at DESC LIMIT 20", {}) + "SELECT name FROM sd_projects WHERE pipeline_id=${s}$ ORDER BY created_at DESC LIMIT 20", + {"s": self.space}) pnames = [getattr(r, "name", "") for r in (all_recs or [])] classify_prompt = f"用户输入: {name}\n项目列表: {', '.join(pnames)}\n\n判断用户想要哪个项目。只回复项目名或\"不存在\"。" matched = await llm_call(classify_prompt, temperature=0.0, org_id=self.org_id) matched = matched.strip().strip('"').strip("'") recs2 = await sor.sqlExe( - "SELECT id, name FROM sd_projects WHERE name=${n}$", {"n": matched}) + "SELECT id, name FROM sd_projects WHERE name=${n}$ AND pipeline_id=${s}$", + {"n": matched, "s": self.space}) if recs2: r = recs2[0] self.project_id = getattr(r, "id", "") @@ -805,17 +814,17 @@ class AgentExecutor: org_id = getattr(_u[0], "orgid", "0") or "0" # 项目专属工作空间目录(workspace_base 从 appbase params 表读,可动态配置) - # 不设置 workspace_dir 会导致 _resolve_workspace 回退到 WORKSPACE_BASE/default, - # 所有项目共用同一目录,设计文档/代码互相覆盖。 - from .workspace import get_workspace_base + # 项目空间 = self.space(通用助手 'general' / 专业产线 pipeline_id),隔离不共享。 + from .workspace import get_workspace_base, build_workspace_path workspace_base = await get_workspace_base(sor) - workspace_dir = os.path.join(workspace_base, str(org_id), name) + pid_val = getID() + workspace_dir = build_workspace_path(workspace_base, org_id, self.space, name, pid_val) os.makedirs(workspace_dir, exist_ok=True) - pid_val = getID() await sor.C("sd_projects", { "id": pid_val, "name": name, "description": desc, - "status": "active", "org_id": org_id, "workspace_dir": workspace_dir, + "status": "active", "org_id": org_id, + "pipeline_id": self.space, "workspace_dir": workspace_dir, }) await sor.C("sd_iterations", { "id": getID(), "project_id": pid_val, diff --git a/pipeline_service/gateway.py b/pipeline_service/gateway.py index 056dad5..f67289d 100644 --- a/pipeline_service/gateway.py +++ b/pipeline_service/gateway.py @@ -155,7 +155,7 @@ class Gateway: config.model_name = ctx["default_llm_name"] executor = AgentExecutor( config=config, project_id=ctx["pid"], user_id=user_id, role=role, - base_url=base_url) + base_url=base_url, generic=generic) # 4. 转发标准化事件流 async for chunk in executor.run(content, history=history): diff --git a/pipeline_service/workspace.py b/pipeline_service/workspace.py index 83bf811..a7caed6 100644 --- a/pipeline_service/workspace.py +++ b/pipeline_service/workspace.py @@ -11,6 +11,26 @@ logger = logging.getLogger(__name__) WORKSPACE_BASE = '/d/pipeline/workspaces' +# 通用助手项目空间键(保留值,不属于任何专业产线;专业产线用 pipelines.id) +GENERAL_SPACE = 'general' + + +def build_workspace_path(workspace_base, org_id, space, name, project_id=''): + """构建项目 workspace 路径:{base}/{org_id}/{space}/{name}。 + + space = 项目空间键(pipeline_id;通用助手为 'general'), + 通用助手与专业产线、不同产线之间由此隔离,不共享目录。 + 同空间重名时追加短 ID 保证唯一,避免多项目共享同一目录互相覆盖。 + """ + space = (space or GENERAL_SPACE).strip() or GENERAL_SPACE + name = (name or 'unnamed').strip() or 'unnamed' + base_dir = os.path.join(workspace_base, str(org_id or '0'), space) + path = os.path.join(base_dir, name) + if project_id and os.path.isdir(path): + short = project_id[-8:] if len(project_id) >= 8 else project_id + path = os.path.join(base_dir, f"{name}_{short}") + return path + # 可编辑的文本文件扩展名 TEXT_EXTENSIONS = { '.md', '.py', '.js', '.html', '.css', '.json', '.txt', @@ -86,7 +106,7 @@ async def get_workspace_dir(sor, uid): return '', workspace_base proj = await sor.sqlExe( - "SELECT name, org_id, workspace_dir FROM sd_projects WHERE id=${p}$", + "SELECT name, org_id, workspace_dir, pipeline_id FROM sd_projects WHERE id=${p}$", {"p": pid}) if not proj: return '', workspace_base @@ -94,9 +114,10 @@ async def get_workspace_dir(sor, uid): pname = getattr(proj[0], 'name', '') org_id = getattr(proj[0], 'org_id', '0') or '0' ws = getattr(proj[0], 'workspace_dir', '') or '' + space = getattr(proj[0], 'pipeline_id', '') or GENERAL_SPACE if ws.startswith('/'): return ws, workspace_base - return workspace_base + '/' + str(org_id) + '/' + pname, workspace_base + return build_workspace_path(workspace_base, org_id, space, pname, pid), workspace_base async def get_workspace_path(sor, user_id): @@ -258,6 +279,8 @@ def load_workspace(): g.get_workspace_base = get_workspace_base g.get_workspace_dir = get_workspace_dir g.get_workspace_path = get_workspace_path + g.build_workspace_path = build_workspace_path + g.GENERAL_SPACE = GENERAL_SPACE g.build_tree_items = build_tree_items g.build_file_widgets = build_file_widgets g.read_file_content = read_file_content