feat(project-space): 项目空间隔离——通用助手('general')与专业产线(pipeline_id)项目空间不共享

- workspace.build_workspace_path: {base}/{org}/{space}/{name},重名追加短ID
- get_workspace_dir 兜底路径加空间段
- AgentExecutor 加 generic 标志派生 self.space;create_project/switch_project 按空间过滤
- gateway 传 generic
This commit is contained in:
ymq 2026-08-19 11:38:23 +08:00
parent 536f51434f
commit 7787898456
3 changed files with 46 additions and 14 deletions

View File

@ -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_urlscheme://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,

View File

@ -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):

View File

@ -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