feat: workspace browser with tree+file list+Wterm editor

This commit is contained in:
ymq 2026-08-12 10:47:58 +08:00
parent 461fbc47b0
commit c8e597764d
3 changed files with 122 additions and 0 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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;i<tasks.length;i++){var t=tasks[i];var card=new bricks.VBox({bgcolor:'#fff',border:'1px solid #e2e8f0',borderRadius:'8px',padding:'10px 14px',gap:'4px',marginBottom:'4px'});var hdr=new bricks.HBox({alignItems:'center',gap:'8px'});var st=t.state||'';var sc='#64748b';if(st==='running')sc='#2563eb';else if(st==='completed')sc='#16a34a';else if(st==='failed')sc='#dc2626';hdr.add_widget(new bricks.Text({text:'\\u25cf',cfontsize:0.8,color:sc,fontWeight:'bold'}));hdr.add_widget(new bricks.Text({text:(t.title||'')+(t.role?' ['+t.role+']':''),cfontsize:0.95,color:'#1e293b',fontWeight:'bold'}));card.add_widget(hdr);if(t.description)card.add_widget(new bricks.Text({text:t.description,cfontsize:0.8,color:'#64748b'}));var meta=new bricks.HBox({gap:'12px'});meta.add_widget(new bricks.Text({text:'\\u72b6\\u6001:'+st,cfontsize:0.75,color:sc}));meta.add_widget(new bricks.Text({text:'v'+t.current_version,cfontsize:0.75,color:'#94a3b8'}));if(t.created_at)meta.add_widget(new bricks.Text({text:t.created_at.substring(0,16),cfontsize:0.75,color:'#94a3b8'}));card.add_widget(meta);vs.add_widget(card);}});})"
}
]
},
{
"widgettype": "Button",
"id": "workspace_btn",
"options": {
"label": "工作空间",
"css": "small"
},
"binds": [
{
"wid": "self",
"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.Tree({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 addNode(parent,node){var children=node.children||[];var files=node.files||[];var hasKids=children.length>0||files.length>0;var n=left.add_node(parent,{text:node.name,data:node});for(var i=0;i<children.length;i++) addNode(n,children[i]);if(!parent) left.expand_all();}addNode(null,data.tree);showFiles(data.tree);left.bind('selected',function(e){var node=e.node;var d=node.data;if(!d)return;pathLabel.set_text(d.path||'');showFiles(d);});refreshBtn.bind('click',function(){loadTree();});});}function showFiles(node){right.clear();var files=node.files||[];if(files.length===0){right.add_widget(new bricks.Text({text:'(空目录)',cfontsize:0.85,color:'#94a3b8'}));return;}for(var i=0;i<files.length;i++){var f=files[i];var row=new bricks.HBox({bgcolor:i%2?'#f8fafc':'#fff',padding:'6px 10px',alignItems:'center',gap:'8px',borderRadius:'4px'});var icon=f.is_text?'📄':f.is_media?'🎬':'📁';row.add_widget(new bricks.Text({text:icon,cfontsize:1.2}));var nameW=new bricks.Text({text:f.name,cfontsize:0.9,color:'#1e293b',css:'filler'});row.add_widget(nameW);var sizeW=new bricks.Text({text:(f.size/1024).toFixed(1)+'KB',cfontsize:0.75,color:'#94a3b8'});row.add_widget(sizeW);(function(fp,fn,ft){row.dom_element.style.cursor='pointer';row.dom_element.addEventListener('click',function(){if(ft){openFile(fp,fn);}else{showFiles(node);}});})(f.path,f.name,f.is_text);right.add_widget(row);}}function openFile(path,name){var fpw=new bricks.PopupWindow({title:name,cwidth:70,cheight:32,auto_open:true});var wterm=new bricks.Wterm({css:'filler',padding:'0'});fpw.content_w.add_widget(wterm);fetch('/pipeline-sdlc/api/workspace_file.dspy?action=read&path='+encodeURIComponent(path)).then(function(r){return r.json()}).then(function(d){if(d.error){wterm.write('Error: '+d.error);return;}wterm.write(d.content||'');var saveBtn=new bricks.Button({label:'保存'});fpw.content_w.add_widget(saveBtn);saveBtn.bind('click',function(){var content=wterm.get_value();fetch('/pipeline-sdlc/api/workspace_file.dspy',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'action=save&path='+encodeURIComponent(path)+'&content='+encodeURIComponent(content)}).then(function(r){return r.json()}).then(function(d){if(d.success){saveBtn.set_text('已保存');setTimeout(function(){saveBtn.set_text('保存');},2000);}});});});}loadTree();});"
}
]
}
],
"binds": []