feat(workspace): 工作空间弹窗适配机构工作空间新结构——树根改项目目录并挂 apps/modules 引用节点,路径解析统一 resolve_workspace_path

This commit is contained in:
ymq 2026-08-24 17:56:48 +08:00
parent 524abb23e6
commit 9ec2ad473d
11 changed files with 94 additions and 53 deletions

View File

@ -13,15 +13,16 @@ session_id = (params_kw or {}).get('session_id', '') or ''
dbname = get_module_dbname('pipeline-sdlc')
async with DBPools().sqlorContext(dbname) as sor:
ws_dir, _ = await get_workspace_dir(sor, uid, session_id)
project_dir, _ = await get_project_dir(sor, uid, session_id)
space_dir, _ = await get_space_dir(sor, uid, session_id)
if not file_id or not ws_dir:
if not file_id or not space_dir:
return {"error": "未指定文件或无工作空间", "markdown": ""}
full_path = ws_dir + '/' + file_id
full_path = resolve_workspace_path(project_dir, space_dir, file_id)
# 路径穿越校验
real_ws = os.path.realpath(ws_dir)
real_ws = os.path.realpath(space_dir)
real_full = os.path.realpath(full_path)
if not real_full.startswith(real_ws + os.sep):
return {"error": "非法路径", "markdown": ""}

View File

@ -15,9 +15,10 @@ session_id = (params_kw or {}).get('session_id', '') or ''
dbname = get_module_dbname('pipeline-sdlc')
async with DBPools().sqlorContext(dbname) as sor:
ws_dir, _ = await get_workspace_dir(sor, uid, session_id)
project_dir, _ = await get_project_dir(sor, uid, session_id)
space_dir, _ = await get_space_dir(sor, uid, session_id)
if not file_id or not ws_dir:
if not file_id or not space_dir:
return {"success": False, "error": "未指定文件或无工作空间"}
if not docdata_str:
@ -28,10 +29,10 @@ try:
except Exception as e:
return {"success": False, "error": f"文档数据解析失败: {e}"}
full_path = ws_dir + '/' + file_id
full_path = resolve_workspace_path(project_dir, space_dir, file_id)
# 路径穿越校验
real_ws = os.path.realpath(ws_dir)
real_ws = os.path.realpath(space_dir)
real_full = os.path.realpath(full_path)
if not real_full.startswith(real_ws + os.sep):
return {"success": False, "error": "非法路径"}

View File

@ -13,15 +13,16 @@ session_id = (params_kw or {}).get('session_id', '') or ''
dbname = get_module_dbname('pipeline-sdlc')
async with DBPools().sqlorContext(dbname) as sor:
ws_dir, _ = await get_workspace_dir(sor, uid, session_id)
project_dir, _ = await get_project_dir(sor, uid, session_id)
space_dir, _ = await get_space_dir(sor, uid, session_id)
if not file_id or file_id == '__root__':
return {"status": "error", "message": "不能删除根目录"}
full_path = ws_dir + '/' + file_id if ws_dir else file_id
full_path = resolve_workspace_path(project_dir, space_dir, file_id)
# 防止路径穿越:确保最终路径在 ws_dir
real_ws = os.path.realpath(ws_dir)
# 防止路径穿越:确保最终路径在机构工作空间space_dir
real_ws = os.path.realpath(space_dir)
real_full = os.path.realpath(full_path)
if not real_full.startswith(real_ws + os.sep):
return {"status": "error", "message": "非法路径"}

View File

@ -16,15 +16,16 @@ session_id = (params_kw or {}).get('session_id', '') or ''
dbname = get_module_dbname('pipeline-sdlc')
async with DBPools().sqlorContext(dbname) as sor:
ws_dir, _ = await get_workspace_dir(sor, uid, session_id)
project_dir, _ = await get_project_dir(sor, uid, session_id)
space_dir, _ = await get_space_dir(sor, uid, session_id)
if not file_id or file_id == '__root__':
return {"widgettype": "Message", "options": {"title": "错误", "message": "未指定文件"}}
full_path = ws_dir + '/' + file_id if ws_dir else file_id
full_path = resolve_workspace_path(project_dir, space_dir, file_id)
# 路径穿越校验
real_ws = os.path.realpath(ws_dir)
real_ws = os.path.realpath(space_dir)
real_full = os.path.realpath(full_path)
if not real_full.startswith(real_ws + os.sep):
return {"widgettype": "Message", "options": {"title": "错误", "message": "非法路径"}}

View File

@ -14,12 +14,10 @@ session_id = (params_kw or {}).get('session_id', '') or ''
dbname = get_module_dbname('pipeline-sdlc')
async with DBPools().sqlorContext(dbname) as sor:
ws_dir, _ = await get_workspace_dir(sor, uid, session_id)
project_dir, _ = await get_project_dir(sor, uid, session_id)
space_dir, _ = await get_space_dir(sor, uid, session_id)
if not folder_id or folder_id == '__root__':
full_dir = ws_dir
else:
full_dir = ws_dir + '/' + folder_id if ws_dir else folder_id
full_dir = resolve_workspace_path(project_dir, space_dir, folder_id)
if not os.path.isdir(full_dir):
return {"widgettype": "Text", "options": {"text": "目录不可用: " + folder_id, "cfontsize": 0.9}}

View File

@ -15,12 +15,13 @@ session_id = (params_kw or {}).get('session_id', '') or ''
dbname = get_module_dbname('pipeline-sdlc')
async with DBPools().sqlorContext(dbname) as sor:
ws_dir, _ = await get_workspace_dir(sor, uid, session_id)
project_dir, _ = await get_project_dir(sor, uid, session_id)
space_dir, _ = await get_space_dir(sor, uid, session_id)
if not file_id:
return {"widgettype": "Message", "options": {"title": "打开失败", "message": "未指定文件"}}
full_path = ws_dir + '/' + file_id if ws_dir else file_id
full_path = resolve_workspace_path(project_dir, space_dir, file_id)
if not os.path.isfile(full_path):
return {"widgettype": "Message", "options": {"title": "打开失败", "message": "文件不存在: " + file_id}}

View File

@ -13,11 +13,11 @@ session_id = (params_kw or {}).get('session_id', '') or ''
_sid_q = ("?session_id=" + session_id) if session_id else ""
async with DBPools().sqlorContext(dbname) as sor:
ws_dir, _ = await get_workspace_dir(sor, uid, session_id)
project_dir, _ = await get_project_dir(sor, uid, session_id)
pname = os.path.basename(ws_dir) if ws_dir else ''
pname = os.path.basename(project_dir) if project_dir else ''
if not ws_dir:
if not project_dir:
resp = {
"widgettype": "PopupWindow",
"options": {"title": "提示", "cwidth": 30, "cheight": 8, "auto_open": True},

View File

@ -1,4 +1,4 @@
# workspace_tree.dspy - 初始只返回根节点无children靠点击展开
# workspace_tree.dspy - 项目目录树:根=项目目录,额外挂项目用到的 apps/modules 子目录
import os
@ -13,11 +13,12 @@ session_id = (params_kw or {}).get('session_id', '') or ''
dbname = get_module_dbname('pipeline-sdlc')
async with DBPools().sqlorContext(dbname) as sor:
ws_dir, _ = await get_workspace_dir(sor, uid, session_id)
project_dir, _ = await get_project_dir(sor, uid, session_id)
space_dir, _ = await get_space_dir(sor, uid, session_id)
if not ws_dir or not os.path.isdir(ws_dir):
if not project_dir or not os.path.isdir(project_dir):
if not node_id:
return [{"id": "__root__", "parentid": "", "label": "📁 工作空间(不可用)", "path": ws_dir, "is_leaf": False}]
return [{"id": "__root__", "parentid": "", "label": "📁 项目目录(不可用)", "path": project_dir, "is_leaf": False}]
return []
# 初始加载:只返回根节点
@ -25,35 +26,69 @@ if not node_id:
return [{
"id": "__root__",
"parentid": "",
"label": "📁 " + os.path.basename(ws_dir),
"path": ws_dir,
"label": "📁 " + os.path.basename(project_dir),
"path": project_dir,
"is_leaf": False
}]
# 展开节点__root__ 特殊处理,其余用相对路径
# 展开节点__root__ 特殊处理(加 apps/modules 引用),@apps/@modules 扫描机构工作空间,其余扫描项目目录
if node_id == '__root__':
scan_path = ws_dir
scan_path = project_dir
id_prefix = ''
else:
scan_path = ws_dir + '/' + node_id
extra_children = True
elif node_id.startswith('@apps/'):
scan_path = os.path.join(space_dir, 'apps', node_id[len('@apps/'):])
id_prefix = node_id + '/'
extra_children = False
elif node_id.startswith('@modules/'):
scan_path = os.path.join(space_dir, 'modules', node_id[len('@modules/'):])
id_prefix = node_id + '/'
extra_children = False
else:
scan_path = resolve_workspace_path(project_dir, space_dir, node_id)
id_prefix = node_id + '/'
extra_children = False
if not os.path.isdir(scan_path):
return []
items = []
# 根节点:额外挂项目用到的 apps 子目录 + modules 子目录
if extra_children:
apps, modules = await get_project_apps_modules(sor, uid, session_id)
for app in apps:
app_path = os.path.join(space_dir, 'apps', app)
if os.path.isdir(app_path):
items.append({
"id": "@apps/" + app,
"parentid": node_id,
"label": "📱 " + app,
"path": app_path,
"is_leaf": False
})
for m in modules:
m_path = os.path.join(space_dir, 'modules', m)
if os.path.isdir(m_path):
items.append({
"id": "@modules/" + m,
"parentid": node_id,
"label": "📦 " + m,
"path": m_path,
"is_leaf": False
})
# 扫描常规内容(子目录)
try:
entries = sorted(os.listdir(scan_path))
except Exception:
return []
entries = []
for name in entries:
full = os.path.join(scan_path, name)
if name.startswith('.') or name == '__pycache__':
continue
if os.path.isdir(full):
# 叶子判定:目录下是否还有「可见」子目录(排除隐藏目录和 __pycache__与下方扫描一致
# 无可见子目录 → 叶子(不可展开)
has_subdir = False
try:
for sub in os.listdir(full):

View File

@ -16,15 +16,13 @@ session_id = (params_kw or {}).get('session_id', '') or ''
dbname = get_module_dbname('pipeline-sdlc')
async with DBPools().sqlorContext(dbname) as sor:
ws_dir, _ = await get_workspace_dir(sor, uid, session_id)
project_dir, _ = await get_project_dir(sor, uid, session_id)
space_dir, _ = await get_space_dir(sor, uid, session_id)
if not ws_dir or not os.path.isdir(ws_dir):
return {"status": "error", "message": "工作空间目录不可用"}
if not project_dir or not os.path.isdir(project_dir):
return {"status": "error", "message": "项目目录不可用"}
if not folder_id or folder_id == '__root__':
target_dir = ws_dir
else:
target_dir = ws_dir + '/' + folder_id
target_dir = resolve_workspace_path(project_dir, space_dir, folder_id)
if not os.path.isdir(target_dir):
return {"status": "error", "message": "目标目录不存在: " + folder_id}

View File

@ -15,15 +15,16 @@ session_id = (params_kw or {}).get('session_id', '') or ''
dbname = get_module_dbname('pipeline-sdlc')
async with DBPools().sqlorContext(dbname) as sor:
ws_dir, _ = await get_workspace_dir(sor, uid, session_id)
project_dir, _ = await get_project_dir(sor, uid, session_id)
space_dir, _ = await get_space_dir(sor, uid, session_id)
if not file_id or file_id == '__root__':
return {"widgettype": "Message", "options": {"title": "查看失败", "message": "未指定文件"}}
full_path = ws_dir + '/' + file_id if ws_dir else file_id
full_path = resolve_workspace_path(project_dir, space_dir, file_id)
# 路径穿越校验
real_ws = os.path.realpath(ws_dir)
real_ws = os.path.realpath(space_dir)
real_full = os.path.realpath(full_path)
if not real_full.startswith(real_ws + os.sep):
return {"widgettype": "Message", "options": {"title": "错误", "message": "非法路径"}}

View File

@ -13,19 +13,20 @@ session_id = (params_kw or {}).get('session_id', '') or ''
dbname = get_module_dbname('pipeline-sdlc')
async with DBPools().sqlorContext(dbname) as sor:
ws_dir, _ = await get_workspace_dir(sor, uid, session_id)
project_dir, _ = await get_project_dir(sor, uid, session_id)
space_dir, _ = await get_space_dir(sor, uid, session_id)
if not file_id or not ws_dir:
if not file_id or not space_dir:
r = DictObject()
r.host = 'localhost'
r.username = 'pipeline'
r.cmdargs = ['echo 未指定文件']
return r
full_path = ws_dir + '/' + file_id
full_path = resolve_workspace_path(project_dir, space_dir, file_id)
# 路径穿越校验
real_ws = os.path.realpath(ws_dir)
real_ws = os.path.realpath(space_dir)
real_full = os.path.realpath(full_path)
if not real_full.startswith(real_ws + os.sep):
r = DictObject()
@ -34,6 +35,9 @@ if not real_full.startswith(real_ws + os.sep):
r.cmdargs = ['echo 非法路径']
return r
# 相对 space_dir 的路径bwrap 挂载 space_dir 到 /homevi 用相对路径)
rel_path = os.path.relpath(full_path, space_dir)
# bwrap 沙箱路径(不存在则拒绝终端,安全优先,不给完整 shell
bwrap = shutil.which('bwrap')
if not bwrap and os.path.exists('/d/pipeline/pipeline-app/bin/bwrap'):
@ -65,7 +69,7 @@ cmd = (
' --bind ' + shlex.quote(real_ws) + ' /home'
' --chdir /home'
' --setenv HOME /home'
' -- vi ' + shlex.quote(file_id)
' -- vi ' + shlex.quote(rel_path)
)
r.cmdargs = [cmd]
return r