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')
|
dbname = get_module_dbname('pipeline-sdlc')
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
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": ""}
|
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)
|
real_full = os.path.realpath(full_path)
|
||||||
if not real_full.startswith(real_ws + os.sep):
|
if not real_full.startswith(real_ws + os.sep):
|
||||||
return {"error": "非法路径", "markdown": ""}
|
return {"error": "非法路径", "markdown": ""}
|
||||||
|
|||||||
@ -15,9 +15,10 @@ session_id = (params_kw or {}).get('session_id', '') or ''
|
|||||||
dbname = get_module_dbname('pipeline-sdlc')
|
dbname = get_module_dbname('pipeline-sdlc')
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
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": "未指定文件或无工作空间"}
|
return {"success": False, "error": "未指定文件或无工作空间"}
|
||||||
|
|
||||||
if not docdata_str:
|
if not docdata_str:
|
||||||
@ -28,10 +29,10 @@ try:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"success": False, "error": f"文档数据解析失败: {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)
|
real_full = os.path.realpath(full_path)
|
||||||
if not real_full.startswith(real_ws + os.sep):
|
if not real_full.startswith(real_ws + os.sep):
|
||||||
return {"success": False, "error": "非法路径"}
|
return {"success": False, "error": "非法路径"}
|
||||||
|
|||||||
@ -13,15 +13,16 @@ session_id = (params_kw or {}).get('session_id', '') or ''
|
|||||||
dbname = get_module_dbname('pipeline-sdlc')
|
dbname = get_module_dbname('pipeline-sdlc')
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
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__':
|
if not file_id or file_id == '__root__':
|
||||||
return {"status": "error", "message": "不能删除根目录"}
|
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 内
|
# 防止路径穿越:确保最终路径在机构工作空间(space_dir)内
|
||||||
real_ws = os.path.realpath(ws_dir)
|
real_ws = os.path.realpath(space_dir)
|
||||||
real_full = os.path.realpath(full_path)
|
real_full = os.path.realpath(full_path)
|
||||||
if not real_full.startswith(real_ws + os.sep):
|
if not real_full.startswith(real_ws + os.sep):
|
||||||
return {"status": "error", "message": "非法路径"}
|
return {"status": "error", "message": "非法路径"}
|
||||||
|
|||||||
@ -16,15 +16,16 @@ session_id = (params_kw or {}).get('session_id', '') or ''
|
|||||||
dbname = get_module_dbname('pipeline-sdlc')
|
dbname = get_module_dbname('pipeline-sdlc')
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
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__':
|
if not file_id or file_id == '__root__':
|
||||||
return {"widgettype": "Message", "options": {"title": "错误", "message": "未指定文件"}}
|
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)
|
real_full = os.path.realpath(full_path)
|
||||||
if not real_full.startswith(real_ws + os.sep):
|
if not real_full.startswith(real_ws + os.sep):
|
||||||
return {"widgettype": "Message", "options": {"title": "错误", "message": "非法路径"}}
|
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')
|
dbname = get_module_dbname('pipeline-sdlc')
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
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 = resolve_workspace_path(project_dir, space_dir, folder_id)
|
||||||
full_dir = ws_dir
|
|
||||||
else:
|
|
||||||
full_dir = ws_dir + '/' + folder_id if ws_dir else folder_id
|
|
||||||
|
|
||||||
if not os.path.isdir(full_dir):
|
if not os.path.isdir(full_dir):
|
||||||
return {"widgettype": "Text", "options": {"text": "目录不可用: " + folder_id, "cfontsize": 0.9}}
|
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')
|
dbname = get_module_dbname('pipeline-sdlc')
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
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:
|
if not file_id:
|
||||||
return {"widgettype": "Message", "options": {"title": "打开失败", "message": "未指定文件"}}
|
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):
|
if not os.path.isfile(full_path):
|
||||||
return {"widgettype": "Message", "options": {"title": "打开失败", "message": "文件不存在: " + file_id}}
|
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 ""
|
_sid_q = ("?session_id=" + session_id) if session_id else ""
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
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 = {
|
resp = {
|
||||||
"widgettype": "PopupWindow",
|
"widgettype": "PopupWindow",
|
||||||
"options": {"title": "提示", "cwidth": 30, "cheight": 8, "auto_open": True},
|
"options": {"title": "提示", "cwidth": 30, "cheight": 8, "auto_open": True},
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# workspace_tree.dspy - 初始只返回根节点(无children),靠点击展开
|
# workspace_tree.dspy - 项目目录树:根=项目目录,额外挂项目用到的 apps/modules 子目录
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@ -13,11 +13,12 @@ session_id = (params_kw or {}).get('session_id', '') or ''
|
|||||||
dbname = get_module_dbname('pipeline-sdlc')
|
dbname = get_module_dbname('pipeline-sdlc')
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
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:
|
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 []
|
return []
|
||||||
|
|
||||||
# 初始加载:只返回根节点
|
# 初始加载:只返回根节点
|
||||||
@ -25,35 +26,69 @@ if not node_id:
|
|||||||
return [{
|
return [{
|
||||||
"id": "__root__",
|
"id": "__root__",
|
||||||
"parentid": "",
|
"parentid": "",
|
||||||
"label": "📁 " + os.path.basename(ws_dir),
|
"label": "📁 " + os.path.basename(project_dir),
|
||||||
"path": ws_dir,
|
"path": project_dir,
|
||||||
"is_leaf": False
|
"is_leaf": False
|
||||||
}]
|
}]
|
||||||
|
|
||||||
# 展开节点:__root__ 特殊处理,其余用相对路径
|
# 展开节点:__root__ 特殊处理(加 apps/modules 引用),@apps/@modules 扫描机构工作空间,其余扫描项目目录
|
||||||
if node_id == '__root__':
|
if node_id == '__root__':
|
||||||
scan_path = ws_dir
|
scan_path = project_dir
|
||||||
id_prefix = ''
|
id_prefix = ''
|
||||||
else:
|
extra_children = True
|
||||||
scan_path = ws_dir + '/' + node_id
|
elif node_id.startswith('@apps/'):
|
||||||
|
scan_path = os.path.join(space_dir, 'apps', node_id[len('@apps/'):])
|
||||||
id_prefix = node_id + '/'
|
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):
|
if not os.path.isdir(scan_path):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
items = []
|
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:
|
try:
|
||||||
entries = sorted(os.listdir(scan_path))
|
entries = sorted(os.listdir(scan_path))
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
entries = []
|
||||||
|
|
||||||
for name in entries:
|
for name in entries:
|
||||||
full = os.path.join(scan_path, name)
|
full = os.path.join(scan_path, name)
|
||||||
if name.startswith('.') or name == '__pycache__':
|
if name.startswith('.') or name == '__pycache__':
|
||||||
continue
|
continue
|
||||||
if os.path.isdir(full):
|
if os.path.isdir(full):
|
||||||
# 叶子判定:目录下是否还有「可见」子目录(排除隐藏目录和 __pycache__,与下方扫描一致)。
|
|
||||||
# 无可见子目录 → 叶子(不可展开)
|
|
||||||
has_subdir = False
|
has_subdir = False
|
||||||
try:
|
try:
|
||||||
for sub in os.listdir(full):
|
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')
|
dbname = get_module_dbname('pipeline-sdlc')
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
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):
|
||||||
return {"status": "error", "message": "工作空间目录不可用"}
|
return {"status": "error", "message": "项目目录不可用"}
|
||||||
|
|
||||||
if not folder_id or folder_id == '__root__':
|
target_dir = resolve_workspace_path(project_dir, space_dir, folder_id)
|
||||||
target_dir = ws_dir
|
|
||||||
else:
|
|
||||||
target_dir = ws_dir + '/' + folder_id
|
|
||||||
|
|
||||||
if not os.path.isdir(target_dir):
|
if not os.path.isdir(target_dir):
|
||||||
return {"status": "error", "message": "目标目录不存在: " + folder_id}
|
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')
|
dbname = get_module_dbname('pipeline-sdlc')
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
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__':
|
if not file_id or file_id == '__root__':
|
||||||
return {"widgettype": "Message", "options": {"title": "查看失败", "message": "未指定文件"}}
|
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)
|
real_full = os.path.realpath(full_path)
|
||||||
if not real_full.startswith(real_ws + os.sep):
|
if not real_full.startswith(real_ws + os.sep):
|
||||||
return {"widgettype": "Message", "options": {"title": "错误", "message": "非法路径"}}
|
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')
|
dbname = get_module_dbname('pipeline-sdlc')
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
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 = DictObject()
|
||||||
r.host = 'localhost'
|
r.host = 'localhost'
|
||||||
r.username = 'pipeline'
|
r.username = 'pipeline'
|
||||||
r.cmdargs = ['echo 未指定文件']
|
r.cmdargs = ['echo 未指定文件']
|
||||||
return r
|
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)
|
real_full = os.path.realpath(full_path)
|
||||||
if not real_full.startswith(real_ws + os.sep):
|
if not real_full.startswith(real_ws + os.sep):
|
||||||
r = DictObject()
|
r = DictObject()
|
||||||
@ -34,6 +35,9 @@ if not real_full.startswith(real_ws + os.sep):
|
|||||||
r.cmdargs = ['echo 非法路径']
|
r.cmdargs = ['echo 非法路径']
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
# 相对 space_dir 的路径(bwrap 挂载 space_dir 到 /home,vi 用相对路径)
|
||||||
|
rel_path = os.path.relpath(full_path, space_dir)
|
||||||
|
|
||||||
# bwrap 沙箱路径(不存在则拒绝终端,安全优先,不给完整 shell)
|
# bwrap 沙箱路径(不存在则拒绝终端,安全优先,不给完整 shell)
|
||||||
bwrap = shutil.which('bwrap')
|
bwrap = shutil.which('bwrap')
|
||||||
if not bwrap and os.path.exists('/d/pipeline/pipeline-app/bin/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'
|
' --bind ' + shlex.quote(real_ws) + ' /home'
|
||||||
' --chdir /home'
|
' --chdir /home'
|
||||||
' --setenv HOME /home'
|
' --setenv HOME /home'
|
||||||
' -- vi ' + shlex.quote(file_id)
|
' -- vi ' + shlex.quote(rel_path)
|
||||||
)
|
)
|
||||||
r.cmdargs = [cmd]
|
r.cmdargs = [cmd]
|
||||||
return r
|
return r
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user