diff --git a/wwwroot/api/office_read.dspy b/wwwroot/api/office_read.dspy index e4d462a..21825b3 100644 --- a/wwwroot/api/office_read.dspy +++ b/wwwroot/api/office_read.dspy @@ -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": ""} diff --git a/wwwroot/api/office_save.dspy b/wwwroot/api/office_save.dspy index 42ef18e..a882b19 100644 --- a/wwwroot/api/office_save.dspy +++ b/wwwroot/api/office_save.dspy @@ -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": "非法路径"} diff --git a/wwwroot/api/workspace_delete.dspy b/wwwroot/api/workspace_delete.dspy index 198b7f2..a44be70 100644 --- a/wwwroot/api/workspace_delete.dspy +++ b/wwwroot/api/workspace_delete.dspy @@ -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": "非法路径"} diff --git a/wwwroot/api/workspace_file.dspy b/wwwroot/api/workspace_file.dspy index 6ff647c..88dfbbe 100644 --- a/wwwroot/api/workspace_file.dspy +++ b/wwwroot/api/workspace_file.dspy @@ -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": "非法路径"}} diff --git a/wwwroot/api/workspace_files.dspy b/wwwroot/api/workspace_files.dspy index b97c07d..7350d13 100644 --- a/wwwroot/api/workspace_files.dspy +++ b/wwwroot/api/workspace_files.dspy @@ -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}} diff --git a/wwwroot/api/workspace_open.dspy b/wwwroot/api/workspace_open.dspy index 24f1a4c..062488d 100644 --- a/wwwroot/api/workspace_open.dspy +++ b/wwwroot/api/workspace_open.dspy @@ -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}} diff --git a/wwwroot/api/workspace_popup.dspy b/wwwroot/api/workspace_popup.dspy index 0fce5a7..e59a666 100644 --- a/wwwroot/api/workspace_popup.dspy +++ b/wwwroot/api/workspace_popup.dspy @@ -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}, diff --git a/wwwroot/api/workspace_tree.dspy b/wwwroot/api/workspace_tree.dspy index 232be20..147ee35 100644 --- a/wwwroot/api/workspace_tree.dspy +++ b/wwwroot/api/workspace_tree.dspy @@ -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): diff --git a/wwwroot/api/workspace_upload.dspy b/wwwroot/api/workspace_upload.dspy index 5e1b33f..9f8f9ea 100644 --- a/wwwroot/api/workspace_upload.dspy +++ b/wwwroot/api/workspace_upload.dspy @@ -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} diff --git a/wwwroot/api/workspace_view.dspy b/wwwroot/api/workspace_view.dspy index 8467cec..83c0c97 100644 --- a/wwwroot/api/workspace_view.dspy +++ b/wwwroot/api/workspace_view.dspy @@ -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": "非法路径"}} diff --git a/wwwroot/workspace_edit.xterm b/wwwroot/workspace_edit.xterm index 885a80d..318ca40 100644 --- a/wwwroot/workspace_edit.xterm +++ b/wwwroot/workspace_edit.xterm @@ -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