# workspace_tree.dspy - 项目目录树:根=项目目录,额外挂项目用到的 apps/modules 子目录 import os node_id = (params_kw or {}).get('id', '').strip() # 前端 id 可能被多层编码,循环解码(2026-09-03 中文目录名根因) from urllib.parse import unquote as _unq for _ in range(4): if '%' not in node_id: break _s2 = _unq(node_id) if _s2 == node_id: break node_id = _s2 uid = await get_user() if not uid: uid = 'user-01' session_id = (params_kw or {}).get('session_id', '') or '' pipeline_id = (params_kw or {}).get('pipeline_id', '') or '' dbname = get_module_dbname('pipeline-sdlc') async with DBPools().sqlorContext(dbname) as sor: # 产线隔离:跨产线项目视为无项目(与弹窗入口一致,防绕过) project_dir, _ = await get_project_dir_pl(sor, uid, session_id, pipeline_id) space_dir, _ = await get_space_dir(sor, uid, session_id) # 通用会话(pipeline_id=_generic):锁用户专属目录 _general/{uid}, # 不进入任何项目的工作空间(2026-09-08 用户要求) if pipeline_id == '_generic': _gd = generic_workspace_dir(uid) os.makedirs(_gd, exist_ok=True) project_dir = _gd space_dir = _gd # 提前读出项目用到的 apps/modules(sor 在块内有效,块外调用会因 sor 关闭返回空) _apps, _modules = await get_project_apps_modules(sor, uid, session_id) if not project_dir or not os.path.isdir(project_dir): if not node_id: return [{"id": "__root__", "parentid": "", "label": "📁 项目目录(不可用)", "path": project_dir, "is_leaf": False}] return [] # 初始加载:只返回根节点 if not node_id: return [{ "id": "__root__", "parentid": "", "label": "📁 " + os.path.basename(project_dir), "path": project_dir, "is_leaf": False }] # 展开节点:__root__ 特殊处理(加 apps/modules 引用),@apps/@modules 扫描机构工作空间,其余扫描项目目录 if node_id == '__root__': scan_path = project_dir id_prefix = '' 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 = _apps, _modules 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: entries = [] for name in entries: full = os.path.join(scan_path, name) if name.startswith('.') or name == '__pycache__': continue if os.path.isdir(full): has_subdir = False try: for sub in os.listdir(full): if sub.startswith('.') or sub == '__pycache__': continue if os.path.isdir(os.path.join(full, sub)): has_subdir = True break except Exception: pass items.append({ "id": id_prefix + name, "parentid": node_id, "label": "📁 " + name, "path": full, "is_leaf": not has_subdir }) return items