diff --git a/wwwroot/api/workspace_delete.dspy b/wwwroot/api/workspace_delete.dspy new file mode 100644 index 0000000..399ee92 --- /dev/null +++ b/wwwroot/api/workspace_delete.dspy @@ -0,0 +1,49 @@ +# workspace_delete.dspy - 删除工作空间文件 + +import os + +file_id = (params_kw or {}).get('id', '').strip() + +uid = await get_user() +if not uid: + uid = 'user-01' + +dbname = get_module_dbname('pipeline-sdlc') +workspace_base = '/d/pipeline/workspaces' + +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: + ws = getattr(proj[0], 'workspace_dir', '') or '' + if ws.startswith('/'): + ws_dir = ws + else: + pname = getattr(proj[0], 'name', '') + org_id = getattr(proj[0], 'org_id', '0') or '0' + ws_dir = workspace_base + '/' + org_id + '/' + pname + +if not file_id or file_id == '__root__': + return {"status": "error", "message": "不能删除根目录"} + +full_path = ws_dir + '/' + file_id if ws_dir else file_id + +# 防止路径穿越:确保最终路径在 ws_dir 内 +real_ws = os.path.realpath(ws_dir) +real_full = os.path.realpath(full_path) +if not real_full.startswith(real_ws + os.sep): + return {"status": "error", "message": "非法路径"} + +if not os.path.isfile(full_path): + return {"status": "error", "message": "文件不存在: " + file_id} + +try: + os.remove(full_path) +except Exception as e: + return {"status": "error", "message": "删除失败: " + str(e)} + +return {"status": "ok", "message": "已删除: " + os.path.basename(full_path)} diff --git a/wwwroot/api/workspace_files.dspy b/wwwroot/api/workspace_files.dspy index 2bb4fa9..a9c33cf 100644 --- a/wwwroot/api/workspace_files.dspy +++ b/wwwroot/api/workspace_files.dspy @@ -1,6 +1,7 @@ -# workspace_files.dspy - 返回目录文件列表(无 id 或 id=__root__ 时返回根目录文件) +# workspace_files.dspy - 目录文件列表(拖拽上传区 + 可选中文件行) import os +import json as _json folder_id = (params_kw or {}).get('id', '').strip() @@ -27,7 +28,6 @@ async with DBPools().sqlorContext(dbname) as sor: org_id = getattr(proj[0], 'org_id', '0') or '0' ws_dir = workspace_base + '/' + org_id + '/' + pname -# 无 id 或 __root__ 都指向工作空间根目录 if not folder_id or folder_id == '__root__': full_dir = ws_dir else: @@ -36,6 +36,35 @@ else: if not os.path.isdir(full_dir): return {"widgettype": "Text", "options": {"text": "目录不可用: " + folder_id, "cfontsize": 0.9}} +upload_url = entire_url("/pipeline-sdlc/api/workspace_upload.dspy") + +# 拖拽上传脚本:读取文件 → base64 → POST → 刷新浏览器 +upload_script = ( + "var fs=event.params.files;" + "if(!fs||!fs.length)return;" + "for(var i=0;i 200000: + content = content[:200000] + "\n\n...(内容过长,已截断)" + +return { + "widgettype": "PopupWindow", + "options": {"title": name, "width": "80%", "height": "80%", "auto_open": True}, + "subwidgets": [{ + "widgettype": "VScrollPanel", + "options": {"css": "filler", "padding": "12px"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": content, "cfontsize": 0.85, "wrap": True}} + ] + }] +} diff --git a/wwwroot/api/workspace_popup.dspy b/wwwroot/api/workspace_popup.dspy index 97c1e53..252884e 100644 --- a/wwwroot/api/workspace_popup.dspy +++ b/wwwroot/api/workspace_popup.dspy @@ -1,4 +1,6 @@ -# workspace_popup.dspy - 返回 PopupWindow,内含 ResourceBrowser 控件 +# workspace_popup.dspy - 返回 PopupWindow,内含 ResourceBrowser(含打开/删除 tools) + +import json as _json uid = await get_user() if not uid: @@ -28,6 +30,29 @@ async with DBPools().sqlorContext(dbname) as sor: }] } else: + open_url = entire_url("/pipeline-sdlc/api/workspace_open.dspy") + delete_url = entire_url("/pipeline-sdlc/api/workspace_delete.dspy") + + # 打开:预览选中文件 + open_script = ( + "if(!bricks.app._ws_sel)return;" + "var r=await fetch(" + _json.dumps(open_url) + "+'?id='+encodeURIComponent(bricks.app._ws_sel.id));" + "var d=await r.json();" + "bricks.widgetBuild(d,bricks.app);" + ) + + # 删除:删除选中文件并刷新 + delete_script = ( + "if(!bricks.app._ws_sel)return;" + "var resp=await fetch(" + _json.dumps(delete_url) + ",{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:bricks.app._ws_sel.id})});" + "var rj=await resp.json();" + "if(rj.status=='ok'){" + "bricks.app._ws_sel=null;bricks.app._ws_sel_el=null;" + "var rb=bricks.getWidgetById('ws_rb',bricks.app);" + "if(rb){var cid=rb.current_id;rb.current_id=undefined;await rb.render_browser(cid);}" + "}else{var m=new bricks.Message({title:'删除失败',message:rj.message||'未知错误'});m.open();}" + ) + resp = { "widgettype": "PopupWindow", "options": { @@ -38,6 +63,7 @@ async with DBPools().sqlorContext(dbname) as sor: }, "subwidgets": [{ "widgettype": "ResourceBrowser", + "id": "ws_rb", "options": { "tree_options": { "widgettype": "Tree", @@ -52,11 +78,32 @@ async with DBPools().sqlorContext(dbname) as sor: "widgettype": "urlwidget", "options": { "url": entire_url("/pipeline-sdlc/api/workspace_files.dspy") - } + }, + "tools": [ + { + "widgettype": "Button", + "options": {"label": "打开", "css": "small"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": open_script}] + }, + { + "widgettype": "Button", + "options": {"label": "删除", "css": "small"}, + "binds": [{ + "wid": "self", "event": "click", "actiontype": "script", "target": "self", + "conform": { + "title": "删除确认", + "message": "确认删除选中的文件?", + "conform": {"label": "删除"}, + "discard": {"label": "取消"} + }, + "script": delete_script + }] + } + ] }, "tree_width": "280px" } }] } - return json.dumps(resp, ensure_ascii=False) + return _json.dumps(resp, ensure_ascii=False) diff --git a/wwwroot/api/workspace_upload.dspy b/wwwroot/api/workspace_upload.dspy new file mode 100644 index 0000000..74d0e59 --- /dev/null +++ b/wwwroot/api/workspace_upload.dspy @@ -0,0 +1,69 @@ +# workspace_upload.dspy - 上传文件到工作空间目录(base64 JSON body) + +import os +import base64 + +folder_id = (params_kw or {}).get('id', '').strip() +filename = (params_kw or {}).get('filename', '').strip() +filedata = (params_kw or {}).get('filedata', '').strip() + +uid = await get_user() +if not uid: + uid = 'user-01' + +dbname = get_module_dbname('pipeline-sdlc') +workspace_base = '/d/pipeline/workspaces' + +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: + ws = getattr(proj[0], 'workspace_dir', '') or '' + if ws.startswith('/'): + ws_dir = ws + else: + pname = getattr(proj[0], 'name', '') + org_id = getattr(proj[0], 'org_id', '0') or '0' + ws_dir = workspace_base + '/' + org_id + '/' + pname + +if not ws_dir or not os.path.isdir(ws_dir): + return {"status": "error", "message": "工作空间目录不可用"} + +if not folder_id or folder_id == '__root__': + target_dir = ws_dir +else: + target_dir = ws_dir + '/' + folder_id + +if not os.path.isdir(target_dir): + return {"status": "error", "message": "目标目录不存在: " + folder_id} + +# 防止路径穿越 +filename = os.path.basename(filename) +if not filename: + return {"status": "error", "message": "文件名无效"} + +if not filedata: + return {"status": "error", "message": "文件内容为空"} + +# 解码 base64 +try: + if filedata.startswith('data:'): + _, b64 = filedata.split(',', 1) + else: + b64 = filedata + data = base64.b64decode(b64) +except Exception as e: + return {"status": "error", "message": "文件解码失败: " + str(e)} + +target_path = os.path.join(target_dir, filename) +try: + with open(target_path, 'wb') as f: + f.write(data) +except Exception as e: + return {"status": "error", "message": "写入失败: " + str(e)} + +return {"status": "ok", "message": "上传成功: " + filename}