feat(workspace): 工作空间弹窗适配机构工作空间新结构——树根改项目目录并挂 apps/modules 引用节点,路径解析统一 resolve_workspace_path
This commit is contained in:
parent
524abb23e6
commit
9ec2ad473d
@ -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": ""}
|
||||
|
||||
@ -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": "非法路径"}
|
||||
|
||||
@ -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": "非法路径"}
|
||||
|
||||
@ -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": "非法路径"}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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},
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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": "非法路径"}}
|
||||
|
||||
@ -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 到 /home,vi 用相对路径)
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user