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")
|
logger = logging.getLogger("pipeline.agent_executor")
|
||||||
|
|
||||||
from .workspace import WORKSPACE_BASE
|
from .workspace import WORKSPACE_BASE, GENERAL_SPACE
|
||||||
|
|
||||||
|
|
||||||
# ── 默认工具定义(在 pipeline-core 未加载时使用)──
|
# ── 默认工具定义(在 pipeline-core 未加载时使用)──
|
||||||
@ -63,6 +63,7 @@ class AgentExecutor:
|
|||||||
model_name: str = None,
|
model_name: str = None,
|
||||||
role: str = "", # 产线内角色(可选,如 develop/design;驾驶舱 agent 为空)
|
role: str = "", # 产线内角色(可选,如 develop/design;驾驶舱 agent 为空)
|
||||||
base_url: str = "", # 请求 base_url(scheme://host/),供 slash 命令生成 widget 绝对 URL
|
base_url: str = "", # 请求 base_url(scheme://host/),供 slash 命令生成 widget 绝对 URL
|
||||||
|
generic: bool = False, # True = 纯通用会话(不解析项目、不挂产线能力)
|
||||||
):
|
):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.project_id = project_id
|
self.project_id = project_id
|
||||||
@ -70,6 +71,8 @@ class AgentExecutor:
|
|||||||
self.org_id = "" # loaded from project context
|
self.org_id = "" # loaded from project context
|
||||||
self.pipeline_id = "" # loaded from project context(产线能力包 key)
|
self.pipeline_id = "" # loaded from project context(产线能力包 key)
|
||||||
self.role = role # 产线内角色(技能/工具/prompt 按角色加载)
|
self.role = role # 产线内角色(技能/工具/prompt 按角色加载)
|
||||||
|
self.generic = generic # 是否纯通用会话
|
||||||
|
self.space = GENERAL_SPACE # 项目空间键:generic→'general',产线→真实 pipeline_id
|
||||||
self.workspace_dir = workspace_dir or WORKSPACE_BASE
|
self.workspace_dir = workspace_dir or WORKSPACE_BASE
|
||||||
self.model_name = model_name or config.model_name
|
self.model_name = model_name or config.model_name
|
||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
@ -362,6 +365,9 @@ class AgentExecutor:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
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):
|
async def _resolve_skills_base_dir(self):
|
||||||
"""技能根目录 = 机构工作目录/skills/(多租户隔离,不在全局应用目录)。"""
|
"""技能根目录 = 机构工作目录/skills/(多租户隔离,不在全局应用目录)。"""
|
||||||
try:
|
try:
|
||||||
@ -755,9 +761,10 @@ class AgentExecutor:
|
|||||||
if not name:
|
if not name:
|
||||||
return "需要项目名称"
|
return "需要项目名称"
|
||||||
|
|
||||||
# 精确匹配
|
# 精确匹配(限定当前项目空间 + 机构)
|
||||||
recs = await sor.sqlExe(
|
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:
|
if recs:
|
||||||
r = recs[0]
|
r = recs[0]
|
||||||
self.project_id = getattr(r, "id", "")
|
self.project_id = getattr(r, "id", "")
|
||||||
@ -769,14 +776,16 @@ class AgentExecutor:
|
|||||||
from pipeline_service.llm_bridge import llm_call
|
from pipeline_service.llm_bridge import llm_call
|
||||||
|
|
||||||
all_recs = await sor.sqlExe(
|
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 [])]
|
pnames = [getattr(r, "name", "") for r in (all_recs or [])]
|
||||||
classify_prompt = f"用户输入: {name}\n项目列表: {', '.join(pnames)}\n\n判断用户想要哪个项目。只回复项目名或\"不存在\"。"
|
classify_prompt = f"用户输入: {name}\n项目列表: {', '.join(pnames)}\n\n判断用户想要哪个项目。只回复项目名或\"不存在\"。"
|
||||||
matched = await llm_call(classify_prompt, temperature=0.0, org_id=self.org_id)
|
matched = await llm_call(classify_prompt, temperature=0.0, org_id=self.org_id)
|
||||||
matched = matched.strip().strip('"').strip("'")
|
matched = matched.strip().strip('"').strip("'")
|
||||||
|
|
||||||
recs2 = await sor.sqlExe(
|
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:
|
if recs2:
|
||||||
r = recs2[0]
|
r = recs2[0]
|
||||||
self.project_id = getattr(r, "id", "")
|
self.project_id = getattr(r, "id", "")
|
||||||
@ -805,17 +814,17 @@ class AgentExecutor:
|
|||||||
org_id = getattr(_u[0], "orgid", "0") or "0"
|
org_id = getattr(_u[0], "orgid", "0") or "0"
|
||||||
|
|
||||||
# 项目专属工作空间目录(workspace_base 从 appbase params 表读,可动态配置)
|
# 项目专属工作空间目录(workspace_base 从 appbase params 表读,可动态配置)
|
||||||
# 不设置 workspace_dir 会导致 _resolve_workspace 回退到 WORKSPACE_BASE/default,
|
# 项目空间 = self.space(通用助手 'general' / 专业产线 pipeline_id),隔离不共享。
|
||||||
# 所有项目共用同一目录,设计文档/代码互相覆盖。
|
from .workspace import get_workspace_base, build_workspace_path
|
||||||
from .workspace import get_workspace_base
|
|
||||||
workspace_base = await get_workspace_base(sor)
|
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)
|
os.makedirs(workspace_dir, exist_ok=True)
|
||||||
|
|
||||||
pid_val = getID()
|
|
||||||
await sor.C("sd_projects", {
|
await sor.C("sd_projects", {
|
||||||
"id": pid_val, "name": name, "description": desc,
|
"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", {
|
await sor.C("sd_iterations", {
|
||||||
"id": getID(), "project_id": pid_val,
|
"id": getID(), "project_id": pid_val,
|
||||||
|
|||||||
@ -155,7 +155,7 @@ class Gateway:
|
|||||||
config.model_name = ctx["default_llm_name"]
|
config.model_name = ctx["default_llm_name"]
|
||||||
executor = AgentExecutor(
|
executor = AgentExecutor(
|
||||||
config=config, project_id=ctx["pid"], user_id=user_id, role=role,
|
config=config, project_id=ctx["pid"], user_id=user_id, role=role,
|
||||||
base_url=base_url)
|
base_url=base_url, generic=generic)
|
||||||
|
|
||||||
# 4. 转发标准化事件流
|
# 4. 转发标准化事件流
|
||||||
async for chunk in executor.run(content, history=history):
|
async for chunk in executor.run(content, history=history):
|
||||||
|
|||||||
@ -11,6 +11,26 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
WORKSPACE_BASE = '/d/pipeline/workspaces'
|
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 = {
|
TEXT_EXTENSIONS = {
|
||||||
'.md', '.py', '.js', '.html', '.css', '.json', '.txt',
|
'.md', '.py', '.js', '.html', '.css', '.json', '.txt',
|
||||||
@ -86,7 +106,7 @@ async def get_workspace_dir(sor, uid):
|
|||||||
return '', workspace_base
|
return '', workspace_base
|
||||||
|
|
||||||
proj = await sor.sqlExe(
|
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})
|
{"p": pid})
|
||||||
if not proj:
|
if not proj:
|
||||||
return '', workspace_base
|
return '', workspace_base
|
||||||
@ -94,9 +114,10 @@ async def get_workspace_dir(sor, uid):
|
|||||||
pname = getattr(proj[0], 'name', '')
|
pname = getattr(proj[0], 'name', '')
|
||||||
org_id = getattr(proj[0], 'org_id', '0') or '0'
|
org_id = getattr(proj[0], 'org_id', '0') or '0'
|
||||||
ws = getattr(proj[0], 'workspace_dir', '') or ''
|
ws = getattr(proj[0], 'workspace_dir', '') or ''
|
||||||
|
space = getattr(proj[0], 'pipeline_id', '') or GENERAL_SPACE
|
||||||
if ws.startswith('/'):
|
if ws.startswith('/'):
|
||||||
return ws, workspace_base
|
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):
|
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_base = get_workspace_base
|
||||||
g.get_workspace_dir = get_workspace_dir
|
g.get_workspace_dir = get_workspace_dir
|
||||||
g.get_workspace_path = get_workspace_path
|
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_tree_items = build_tree_items
|
||||||
g.build_file_widgets = build_file_widgets
|
g.build_file_widgets = build_file_widgets
|
||||||
g.read_file_content = read_file_content
|
g.read_file_content = read_file_content
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user