pipeline-sdlc/wwwroot/api/workspace_files.dspy

144 lines
6.0 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# workspace_files.dspy - 目录文件列表(拖拽上传区 + 可选中文件行)
import os
import json as _json
from urllib.parse import unquote
def _decode_id(s):
for _ in range(4):
if '%' not in s:
break
s2 = unquote(s)
if s2 == s:
break
s = s2
return s
folder_id = _decode_id((params_kw or {}).get('id', '').strip())
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
full_dir = resolve_workspace_path(project_dir, space_dir, folder_id)
if not os.path.isdir(full_dir):
return {"widgettype": "Text", "options": {"otext": "目录不可用: ${p0}", "text": "目录不可用: ${p0}", "i18n": True, "i18n_params": {"p0": str(folder_id)}, "cfontsize": 0.9}}
# 上传端点必须与列表端点同参数session_id + pipeline_id 都要透传。
# 只传 session_id 的旧代码在通用助手pipeline_id=_generic、无 session_id
# 让 workspace_upload.dspy 走不到 _generic 分支,解析到别的项目目录 →
# 上传报「目标目录不存在」2026-09-17 实测根因)。
_uq = []
if session_id:
_uq.append("session_id=" + session_id)
if pipeline_id:
_uq.append("pipeline_id=" + pipeline_id)
upload_url = entire_url("/pipeline-sdlc/api/workspace_upload.dspy") + (("?" + "&".join(_uq)) if _uq else "")
# 拖拽上传脚本:读取文件 → base64 → form-encoded POST → 刷新浏览器
upload_script = (
"var fs=event.params.files;"
"if(!fs||!fs.length)return;"
"for(var i=0;i<fs.length;i++){"
"var f=fs[i].file;"
"var b64=await new Promise(function(res){var r=new FileReader();r.onload=function(){res(r.result);};r.readAsDataURL(f);});"
"var body=new URLSearchParams();"
"body.append('id'," + _json.dumps(folder_id) + ");"
"body.append('filename',f.name);"
"body.append('filedata',b64);"
"var resp=await fetch(" + _json.dumps(upload_url) + ",{method:'POST',body:body});"
"var rj=await resp.json();"
"if(rj.status!='ok'){var m=new bricks.Message({title:'上传失败',message:rj.message||'未知错误'});m.open();}"
"}"
"var rb=bricks.getWidgetById('ws_rb',bricks.app);"
"if(rb){var cid=rb.current_id;rb.current_id=undefined;await rb.render_browser(cid);}"
)
# 拖拽上传区(固定高度)
droppable = {
"widgettype": "Droppable",
"options": {"accepts": ["*"], "padding": "10px"},
"subwidgets": [
{"widgettype": "Text", "options": {"otext": "📥 拖拽文件到此处上传", "text": "📥 拖拽文件到此处上传", "i18n": True, "cfontsize": 0.85, "color": "#64748b", "halign": "middle"}}
],
"binds": [{"wid": "self", "event": "filedrop", "actiontype": "script", "target": "self", "script": upload_script}]
}
files = []
try:
entries = sorted(os.listdir(full_dir))
except Exception as e:
return {"widgettype": "Text", "options": {"text": "读取错误: " + str(e), "cfontsize": 0.9}}
for name in entries:
full = os.path.join(full_dir, 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', '.csv', '.ts', '.tsx', '.jsx',
'.java', '.c', '.cpp', '.h', '.go', '.rs', '.rb', '.php', '.vue', '.scss', '.less']
icon = "📄" if is_text else "🎬" if ext in ['.png', '.jpg', '.jpeg', '.gif', '.mp4', '.mp3', '.wav'] else "📦"
if folder_id and folder_id != '__root__':
rel_path = folder_id + '/' + name
else:
rel_path = name
select_script = (
"if(bricks.app._ws_sel_el)bricks.app._ws_sel_el.classList.remove('selected');"
"this.dom_element.classList.add('selected');"
"bricks.app._ws_sel_el=this.dom_element;"
"bricks.app._ws_sel={id:" + _json.dumps(rel_path) + ",name:" + _json.dumps(name) + "};"
)
row = {
"widgettype": "HBox",
"options": {"padding": "6px 10px", "alignItems": "center", "gap": "8px", "cursor": "pointer"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": icon, "cfontsize": 1.2}},
{"widgettype": "Text", "options": {"text": name, "cfontsize": 0.9, "color": "var(--ink-1)", "halign": "left", "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": select_script}]
}
files.append(row)
if not files:
file_list_content = [{"widgettype": "Text", "options": {"otext": "(空目录)", "text": "(空目录)", "i18n": True, "cfontsize": 0.85, "color": "#94a3b8", "padding": "8px"}}]
else:
file_list_content = files
# 文件列表区filler + VScrollPanel占满剩余高度并可滚动
file_list_panel = {
"widgettype": "VScrollPanel",
"options": {"css": "filler", "width": "100%"},
"subwidgets": file_list_content
}
return {
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "8px", "gap": "6px"},
"subwidgets": [droppable, file_list_panel]
}