From c8e597764df6997d4207ff7c0505191e19b423e0 Mon Sep 17 00:00:00 2001 From: ymq Date: Wed, 12 Aug 2026 10:47:58 +0800 Subject: [PATCH] feat: workspace browser with tree+file list+Wterm editor --- wwwroot/api/workspace_browse.dspy | 67 +++++++++++++++++++++++++++++++ wwwroot/api/workspace_file.dspy | 38 ++++++++++++++++++ wwwroot/index.ui | 17 ++++++++ 3 files changed, 122 insertions(+) create mode 100644 wwwroot/api/workspace_browse.dspy create mode 100644 wwwroot/api/workspace_file.dspy diff --git a/wwwroot/api/workspace_browse.dspy b/wwwroot/api/workspace_browse.dspy new file mode 100644 index 0000000..bd280f1 --- /dev/null +++ b/wwwroot/api/workspace_browse.dspy @@ -0,0 +1,67 @@ +# workspace_browse.dspy - 浏览项目工作空间目录树 + +uid = await get_user() +if not uid: + uid = 'user-01' + +dbname = get_module_dbname('pipeline-sdlc') +workspace_base = '/home/pipeline/pipeline_ws' + +async with DBPools().sqlorContext(dbname) as sor: + # 获取当前项目和工作空间目录 + recs = await sor.sqlExe( + "SELECT current_project_id FROM pipeline_agent_settings WHERE user_id=${u}$", + {"u": uid}) + pid = getattr(recs[0], 'current_project_id', '') if recs else '' + + ws_dir = '' + if pid: + proj = await sor.sqlExe( + "SELECT name, workspace_dir FROM sd_projects WHERE id=${p}$", + {"p": pid}) + if proj: + pname = getattr(proj[0], 'name', '') + ws = getattr(proj[0], 'workspace_dir', '') or '' + ws_dir = ws if ws.startswith('/') else workspace_base + '/' + pname + +# 安全限制 +if not ws_dir or not ws_dir.startswith(workspace_base): + return json.dumps({"error": "工作空间不可用"}, ensure_ascii=False) + +import os + +def build_tree(path, base): + """递归构建目录树""" + result = {"name": os.path.basename(path) or path, "path": path, "children": []} + try: + items = sorted(os.listdir(path)) + except Exception: + return result + + dirs = [] + files = [] + for item in items: + full = os.path.join(path, item) + if os.path.isdir(full) and not item.startswith('.') and item != '__pycache__': + dirs.append(build_tree(full, base)) + elif os.path.isfile(full) and not item.startswith('.'): + size = os.path.getsize(full) + ext = os.path.splitext(item)[1].lower() + files.append({ + "name": item, "path": full, "size": size, "ext": ext, + "is_text": ext in ['.md','.py','.js','.html','.css','.json','.txt', + '.xml','.yaml','.yml','.toml','.cfg','.sh','.sql', + '.dspy','.ui','.java','.go','.rs','.ts','.tsx','.vue'], + "is_media": ext in ['.png','.jpg','.jpeg','.gif','.svg','.mp4','.webm','.mp3','.wav'], + }) + result["children"] = dirs + result["files"] = files + return result + +tree = build_tree(ws_dir, ws_dir) + +return json.dumps({ + "workspace": ws_dir, + "project_id": pid, + "tree": tree, +}, ensure_ascii=False, default=str) diff --git a/wwwroot/api/workspace_file.dspy b/wwwroot/api/workspace_file.dspy new file mode 100644 index 0000000..77d70dc --- /dev/null +++ b/wwwroot/api/workspace_file.dspy @@ -0,0 +1,38 @@ +# workspace_file.dspy - 读取/保存工作空间文件 + +action = (params_kw or {}).get('action', 'read') +filepath = (params_kw or {}).get('path', '').strip() +workspace_base = '/home/pipeline/pipeline_ws' + +# 安全检查 +if not filepath or not filepath.startswith(workspace_base): + return json.dumps({"error": "路径不合法"}, ensure_ascii=False) + +import os + +if action == 'read': + if not os.path.isfile(filepath): + return json.dumps({"error": "文件不存在"}, ensure_ascii=False) + try: + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read(50000) + return json.dumps({ + "path": filepath, + "name": os.path.basename(filepath), + "size": os.path.getsize(filepath), + "content": content, + }, ensure_ascii=False) + except Exception as e: + return json.dumps({"error": str(e)}, ensure_ascii=False) + +elif action == 'save': + content = (params_kw or {}).get('content', '') + try: + with open(filepath, 'w', encoding='utf-8') as f: + f.write(content) + return json.dumps({"success": True, "path": filepath}, ensure_ascii=False) + except Exception as e: + return json.dumps({"error": str(e)}, ensure_ascii=False) + +else: + return json.dumps({"error": "未知操作"}, ensure_ascii=False) diff --git a/wwwroot/index.ui b/wwwroot/index.ui index e1cfea5..5a72889 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -101,6 +101,23 @@ "script": "fetch('/pipeline-sdlc/api/cockpit_context.dspy').then(function(r){return r.json()}).then(function(d){var pid=d.project_id||'';var pn=d.project_name||'';if(!pid){var pw=new bricks.PopupWindow({title:'选择项目',cwidth:32,cheight:16,auto_open:true});var form=new bricks.Form({name:'p_picker',cols:1,fields:[{name:'project_id',uitype:'code',label:'项目',placeholder:'请选择项目',cwidth:26,dataurl:'/pipeline-sdlc/api/cockpit_project_picker.dspy',valueField:'value',textField:'text'}]});var vb=new bricks.VBox({padding:'12px',gap:'12px'});vb.add_widget(new bricks.Text({text:'请选择项目:',cfontsize:0.95,color:'#64748b'}));vb.add_widget(form);pw.content_w.add_widget(vb);setTimeout(function(){var sel=pw.content_w.dom_element.querySelector('select');if(sel)sel.addEventListener('change',function(){var v=sel.value;if(!v)return;fetch('/pipeline-sdlc/api/cockpit_context_update.dspy',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'project_id='+encodeURIComponent(v)}).then(function(r){return r.json()}).then(function(r){if(r.success){pw.destroy();var sel2=bricks.getWidgetById('project_selector',bricks.app);if(sel2)sel2.setValue(r.project_id);var tb=bricks.getWidgetById('task_btn',bricks.app);if(tb&&tb.text_w)tb.text_w.set_text('\\u4efb\\u52a1');}});});},500);return;}var upw=new bricks.PopupWindow({title:pn+' - \\u4efb\\u52a1\\u5217\\u8868',cwidth:60,cheight:30,auto_open:true});var vs=new bricks.VScrollPanel({css:'filler',padding:'12px',gap:'8px'});upw.content_w.add_widget(vs);fetch('/pipeline-sdlc/api/cockpit_project_tasks.dspy').then(function(r){return r.json()}).then(function(data){var tasks=data.tasks||[];if(tasks.length===0){vs.add_widget(new bricks.Text({text:'\\u6682\\u65e0\\u4efb\\u52a1',cfontsize:0.9,color:'#94a3b8'}));return;}for(var i=0;i0||files.length>0;var n=left.add_node(parent,{text:node.name,data:node});for(var i=0;i