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:
parent
536f51434f
commit
7787898456
@ -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,
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user