diff --git a/wwwroot/api/workspace_files.dspy b/wwwroot/api/workspace_files.dspy new file mode 100644 index 0000000..f594bdf --- /dev/null +++ b/wwwroot/api/workspace_files.dspy @@ -0,0 +1,75 @@ +# workspace_files.dspy - 返回目录下文件列表(Bricks widget 格式) + +import os + +folder_id = (params_kw or {}).get('id', '').strip() + +if not folder_id or folder_id == '__root__': + return {"widgettype": "Text", "options": {"text": "请从左侧选择目录", "cfontsize": 0.9}} + +if not os.path.isdir(folder_id): + return {"widgettype": "Text", "options": {"text": "目录不可用"}} + +# 文件列表 +files = [] +try: + entries = sorted(os.listdir(folder_id)) +except Exception as e: + return {"widgettype": "Text", "options": {"text": "读取错误: " + str(e)}} + +for name in entries: + full = os.path.join(folder_id, name) + if name.startswith('.'): + continue + if os.path.isfile(full): + size = os.path.getsize(full) + ext = os.path.splitext(name)[1].lower() + is_text = ext in ['.md','.py','.js','.html','.css','.json','.txt','.xml','.yaml','.yml','.toml','.cfg','.sh','.sql','.dspy','.ui'] + icon = "📄" if is_text else "🎬" if ext in ['.png','.jpg','.mp4'] else "📁" + fpath_escaped = full.replace("\\", "\\\\").replace("'", "\\'") + fname_escaped = name.replace("'", "\\'") + row = { + "widgettype": "HBox", + "options": { + "padding": "6px 10px", + "alignItems": "center", + "gap": "8px", + "borderRadius": "4px", + "style": {"cursor": "pointer"}, + }, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": icon, "cfontsize": 1.2}}, + {"widgettype": "Text", "options": {"text": name, "cfontsize": 0.9, "color": "#1e293b", "css": "filler"}}, + {"widgettype": "Text", "options": {"text": f"{size/1024:.1f}KB", "cfontsize": 0.75, "color": "#94a3b8"}}, + ], + "binds": [{ + "wid": "self", + "event": "click", + "actiontype": "script", + "target": "self", + "script": f"var pw=new bricks.PopupWindow({{title:'{fname_escaped}',cwidth:70,cheight:32,auto_open:true}});var wt=new bricks.Wterm({{css:'filler',padding:'0'}});pw.content_w.add_widget(wt);var sb=new bricks.Button({{label:'保存'}});pw.content_w.add_widget(sb);fetch('/pipeline-sdlc/api/workspace_file.dspy?action=read&path='+encodeURIComponent('{fpath_escaped}')).then(function(r){{return r.json()}}).then(function(d){{if(d.error){{wt.write('Error: '+d.error);return}};wt.write(d.content||'');sb.bind('click',function(){{var c=wt.get_value();fetch('/pipeline-sdlc/api/workspace_file.dspy',{{method:'POST',headers:{{'Content-Type':'application/x-www-form-urlencoded'}},body:'action=save&path='+encodeURIComponent('{fpath_escaped}')+'&content='+encodeURIComponent(c)}}).then(function(r){{return r.json()}}).then(function(d){{if(d.success){{sb.set_text('已保存');setTimeout(function(){{sb.set_text('保存')}},2000)}}}})}});}})" + }] if is_text else [] + } if is_text else { + "widgettype": "HBox", + "options": { + "padding": "6px 10px", + "alignItems": "center", + "gap": "8px", + "borderRadius": "4px", + }, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": icon, "cfontsize": 1.2}}, + {"widgettype": "Text", "options": {"text": name, "cfontsize": 0.9, "color": "#94a3b8", "css": "filler"}}, + {"widgettype": "Text", "options": {"text": f"{size/1024:.1f}KB", "cfontsize": 0.75, "color": "#94a3b8"}}, + ], + } + files.append(row) + +if not files: + return {"widgettype": "Text", "options": {"text": "(空目录)", "cfontsize": 0.85, "color": "#94a3b8"}} + +return { + "widgettype": "VScrollPanel", + "options": {"css": "filler", "padding": "8px", "gap": "4px"}, + "subwidgets": files +} diff --git a/wwwroot/api/workspace_tree.dspy b/wwwroot/api/workspace_tree.dspy new file mode 100644 index 0000000..1ba5bd7 --- /dev/null +++ b/wwwroot/api/workspace_tree.dspy @@ -0,0 +1,49 @@ +# workspace_tree.dspy - 返回 Tree widget 需要的 {id, parentid, label} 格式 + +uid = await get_user() +if not uid: + uid = 'user-01' + +dbname = get_module_dbname('pipeline-sdlc') +workspace_base = '/d/pipeline/workspaces' +import os + +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, org_id, workspace_dir FROM sd_projects WHERE id=${p}$", + {"p": pid}) + if proj: + pname = getattr(proj[0], 'name', '') + org_id = getattr(proj[0], 'org_id', '0') or '0' + ws = getattr(proj[0], 'workspace_dir', '') or '' + ws_dir = ws if ws.startswith('/') else workspace_base + '/' + org_id + '/' + pname + +if not ws_dir or not os.path.isdir(ws_dir): + return [{"id": "__root__", "parentid": "", "label": "📁 工作空间(不可用)"}] + +# 递归扫描目录,生成 {id, parentid, label} +items = [{"id": "__root__", "parentid": "", "label": "📁 " + os.path.basename(ws_dir)}] + +def scan_dir(path, parent_id): + try: + entries = sorted(os.listdir(path)) + except Exception: + return + for name in entries: + full = os.path.join(path, name) + if name.startswith('.') or name == '__pycache__': + continue + if os.path.isdir(full): + dir_id = full # 用全路径做 id + items.append({"id": dir_id, "parentid": parent_id, "label": "📁 " + name}) + scan_dir(full, dir_id) + +scan_dir(ws_dir, "__root__") +return items diff --git a/wwwroot/index.ui b/wwwroot/index.ui index f26037c..c564ba0 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -115,7 +115,7 @@ "event": "click", "actiontype": "script", "target": "self", - "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:30,cheight:8,auto_open:true});pw.content_w.add_widget(new bricks.Text({text:'请先选择项目',cfontsize:1,color:'#64748b',padding:'20px'}));return;}var pw=new bricks.PopupWindow({title:pn+' - 工作空间',cwidth:80,cheight:36,auto_open:true});var hsplit=new bricks.Splitter({direction:'horizontal',split:'25%'});var left=new bricks.VScrollPanel({css:'filler',padding:'4px'});var right=new bricks.VScrollPanel({css:'filler',padding:'8px',gap:'4px'});var btnBar=new bricks.HBox({padding:'4px 8px',gap:'8px',cheight:2});var refreshBtn=new bricks.Button({label:'刷新',css:'small'});btnBar.add_widget(refreshBtn);var pathLabel=new bricks.Text({text:'',cfontsize:0.8,color:'#94a3b8'});btnBar.add_widget(pathLabel);var rightBox=new bricks.VBox({css:'filler',gap:'0'});rightBox.add_widget(btnBar);rightBox.add_widget(right);hsplit.add_widget(left);hsplit.add_widget(rightBox);pw.content_w.add_widget(hsplit);function loadTree(){fetch('/pipeline-sdlc/api/workspace_browse.dspy').then(function(r){return r.json()}).then(function(data){if(data.error){right.add_widget(new bricks.Text({text:data.error,color:'#f44336'}));return;}left.clear();function renderDir(dir,parent,level){var indent=level*16;var row=new bricks.HBox({padding:'3px 6px 3px '+(4+indent)+'px',alignItems:'center',gap:'4px',cursor:'pointer'});row.add_widget(new bricks.Text({text:'📁',cfontsize:1}));row.add_widget(new bricks.Text({text:dir.name,cfontsize:0.85}));row.dom_element.addEventListener('click',function(){pathLabel.set_text(dir.path);showFiles(dir);});(parent||left).add_widget(row);var children=dir.children||[];for(var i=0;i