fix: file_list alignment + view/download/delete actions

- Filename: add textAlign left + cwidth columns for type/size/status/date
- View: newwindow → window.open(_blank)
- Download: urlwidget PopupWindow → script anchor download
- Delete: hardcoded path → relative path, refresh with widget list
This commit is contained in:
yumoqing 2026-07-29 16:41:32 +08:00
parent ac75d75ec4
commit 224496ae0e
2 changed files with 234 additions and 0 deletions

View File

@ -0,0 +1,114 @@
ns = params_kw.copy()
doc_id = ns.get('doc_id', '')
kb_id = ns.get('kb_id', '')
folder_id = ns.get('folder_id', '')
folder_label = ns.get('folder_label', '全部文件')
if not doc_id:
return json.dumps({"status": "error", "error": "doc_id required"}, ensure_ascii=False)
env = request._run_ns
userorgid = await env.get_userorgid()
async with get_sor_context(env, 'rag') as sor:
recs = await sor.sqlExe(
"SELECT file_path, file_size, kb_id FROM documents WHERE id=${id}$",
{"id": doc_id})
if not recs:
return json.dumps({"status": "error", "error": "file not found"}, ensure_ascii=False)
doc = recs[0]
file_path = doc.file_path
file_size = doc.file_size
# Delete document + chunks
await sor.sqlExe("DELETE FROM document_chunks WHERE doc_id=${doc_id}$", {"doc_id": doc_id})
await sor.sqlExe("DELETE FROM documents WHERE id=${id}$", {"id": doc_id})
# Update KB stats
await sor.sqlExe(
"UPDATE knowledge_bases SET doc_count=GREATEST(doc_count-1, 0), total_size=GREATEST(total_size-${size}$, 0) WHERE id=${kb_id}$",
{"size": file_size, "kb_id": kb_id})
# Delete physical file
if file_path and file_path.startswith("/idfile/"):
import os
base = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "rag", "files")
real_path = os.path.join(base, os.path.basename(file_path))
try:
if os.path.exists(real_path):
os.remove(real_path)
except Exception:
pass
# Re-query and return refreshed file list
db = DBPools()
dbname = get_module_dbname('rag')
rows = []
async with db.sqlorContext(dbname) as sor:
if folder_id and folder_id != '__root__':
recs = await sor.sqlExe(
"SELECT id, file_name, file_type, file_size, file_path, status, chunk_count, created_at FROM documents WHERE kb_id=${kb_id}$ AND folder_id=${folder_id}$ ORDER BY created_at DESC",
{"kb_id": kb_id, "folder_id": folder_id})
else:
recs = await sor.sqlExe(
"SELECT id, file_name, file_type, file_size, file_path, status, chunk_count, created_at FROM documents WHERE kb_id=${kb_id}$ AND (folder_id IS NULL OR folder_id='') ORDER BY created_at DESC",
{"kb_id": kb_id})
for r in recs:
rows.append({
"id": r.id, "file_name": r.file_name, "file_type": r.file_type or '',
"file_size": r.file_size or 0, "file_path": r.file_path or '',
"status": r.status or '', "chunk_count": r.chunk_count or 0,
"created_at": str(r.created_at)[:16] if r.created_at else ''
})
def fmt_size(sz):
if sz < 1024: return str(sz) + "B"
elif sz < 1024*1024: return str(round(sz/1024, 1)) + "KB"
else: return str(round(sz/(1024*1024), 1)) + "MB"
header = {
"widgettype": "HBox",
"options": {"width": "100%", "padding": "10px 0", "borderBottom": "1px solid #e0e0e0", "alignItems": "center"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "📄 " + folder_label, "cfontsize": 14, "fontWeight": "bold", "color": "#333"}},
{"widgettype": "Text", "options": {"text": "" + str(len(rows)) + " 个文件)", "cfontsize": 12, "color": "#999", "marginLeft": "8px"}},
{"widgettype": "Text", "options": {"text": "", "css": "filler"}}
]
}
file_widgets = [header]
if not rows:
file_widgets.append({"widgettype": "Text", "options": {"text": "暂无文件", "cfontsize": 13, "color": "#aaa", "halign": "center", "marginTop": "40px"}})
else:
del_url_base = entire_url('./delete_file.dspy')
for f in rows:
sc = "#50b86c"; st = "已完成"
if f["status"] == "pending": sc = "#e6a23c"; st = "处理中"
elif f["status"] == "error": sc = "#f56c6c"; st = "失败"
file_url = entire_url(f["file_path"])
durl = del_url_base + "?kb_id=" + kb_id + "&doc_id=" + f["id"] + "&folder_id=" + folder_id + "&folder_label=" + folder_label
row = {
"widgettype": "HBox",
"options": {"width": "100%", "padding": "8px 12px", "borderBottom": "1px solid #f0f0f0", "alignItems": "center"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "📄 " + f["file_name"], "cfontsize": 13, "color": "#333", "textAlign": "left", "css": "filler"}},
{"widgettype": "Text", "options": {"text": f["file_type"].upper(), "cfontsize": 11, "color": "#999", "marginRight": "8px", "cwidth": 5, "textAlign": "center"}},
{"widgettype": "Text", "options": {"text": fmt_size(f["file_size"]), "cfontsize": 11, "color": "#999", "marginRight": "8px", "cwidth": 8, "textAlign": "right"}},
{"widgettype": "Text", "options": {"text": st, "cfontsize": 11, "color": sc, "marginRight": "8px", "cwidth": 6, "textAlign": "center"}},
{"widgettype": "Text", "options": {"text": f["created_at"], "cfontsize": 11, "color": "#999", "marginRight": "8px", "cwidth": 12}},
{"widgettype": "HBox", "options": {"spacing": "4px"}, "subwidgets": [
{"widgettype": "Text", "options": {"text": "👁", "cfontsize": 14, "css": "clickable", "color": "#4a90d9", "title": "查看"},
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self",
"script": "window.open(" + json.dumps(file_url) + ",'_blank')"}]},
{"widgettype": "Text", "options": {"text": "⬇", "cfontsize": 14, "css": "clickable", "color": "#50b86c", "title": "下载"},
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self",
"script": "var a=document.createElement('a');a.href=" + json.dumps(file_url) + ";a.download='';document.body.appendChild(a);a.click();document.body.removeChild(a)"}]},
{"widgettype": "Text", "options": {"text": "🗑", "cfontsize": 14, "css": "clickable", "color": "#f56c6c", "title": "删除"},
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "file_list_panel", "mode": "replace",
"options": {"url": durl}}]}
]}
]}
file_widgets.append(row)
result = {"widgettype": "VBox", "options": {"width": "100%", "spacing": "0"}, "subwidgets": file_widgets}
return result

View File

@ -0,0 +1,120 @@
ns = params_kw.copy()
kb_id = ns.get('kb_id', '')
folder_id = ns.get('id', '')
folder_label = ns.get('label', '全部文件')
if folder_id == '__root__':
folder_id = ''
folder_label = '根目录'
db = DBPools()
dbname = get_module_dbname('rag')
rows = []
async with db.sqlorContext(dbname) as sor:
if folder_id:
recs = await sor.sqlExe(
"SELECT id, file_name, file_type, file_size, file_path, status, chunk_count, created_at FROM documents WHERE kb_id=${kb_id}$ AND folder_id=${folder_id}$ ORDER BY created_at DESC",
{"kb_id": kb_id, "folder_id": folder_id})
else:
recs = await sor.sqlExe(
"SELECT id, file_name, file_type, file_size, file_path, status, chunk_count, created_at FROM documents WHERE kb_id=${kb_id}$ AND (folder_id IS NULL OR folder_id='') ORDER BY created_at DESC",
{"kb_id": kb_id})
for r in recs:
rows.append({
"id": r.id,
"file_name": r.file_name,
"file_type": r.file_type or '',
"file_size": r.file_size or 0,
"file_path": r.file_path or '',
"status": r.status or '',
"chunk_count": r.chunk_count or 0,
"created_at": str(r.created_at)[:16] if r.created_at else ''
})
def fmt_size(sz):
if sz < 1024:
return str(sz) + "B"
elif sz < 1024*1024:
return str(round(sz/1024, 1)) + "KB"
else:
return str(round(sz/(1024*1024), 1)) + "MB"
header = {
"widgettype": "HBox",
"options": {"width": "100%", "padding": "10px 0", "borderBottom": "1px solid #e0e0e0", "alignItems": "center"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "📄 " + folder_label, "cfontsize": 14, "fontWeight": "bold", "color": "#333"}},
{"widgettype": "Text", "options": {"text": "" + str(len(rows)) + " 个文件)", "cfontsize": 12, "color": "#999", "marginLeft": "8px"}},
{"widgettype": "Text", "options": {"text": "", "css": "filler"}}
]
}
file_widgets = [header]
if not rows:
file_widgets.append({
"widgettype": "Text",
"options": {"text": "暂无文件", "cfontsize": 13, "color": "#aaa", "halign": "center", "marginTop": "40px"}
})
else:
for f in rows:
sc = "#50b86c"
st = "已完成"
if f["status"] == "pending":
sc = "#e6a23c"
st = "处理中"
elif f["status"] == "error":
sc = "#f56c6c"
st = "失败"
file_url = entire_url(f["file_path"])
del_url = entire_url('./delete_file.dspy') + "?kb_id=" + kb_id + "&doc_id=" + f["id"] + "&folder_id=" + folder_id + "&folder_label=" + folder_label
row = {
"widgettype": "HBox",
"options": {"width": "100%", "padding": "8px 12px", "borderBottom": "1px solid #f0f0f0", "alignItems": "center"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "📄 " + f["file_name"], "cfontsize": 13, "color": "#333", "textAlign": "left", "css": "filler"}},
{"widgettype": "Text", "options": {"text": f["file_type"].upper(), "cfontsize": 11, "color": "#999", "marginRight": "8px", "cwidth": 5, "textAlign": "center"}},
{"widgettype": "Text", "options": {"text": fmt_size(f["file_size"]), "cfontsize": 11, "color": "#999", "marginRight": "8px", "cwidth": 8, "textAlign": "right"}},
{"widgettype": "Text", "options": {"text": st, "cfontsize": 11, "color": sc, "marginRight": "8px", "cwidth": 6, "textAlign": "center"}},
{"widgettype": "Text", "options": {"text": f["created_at"], "cfontsize": 11, "color": "#999", "marginRight": "8px", "cwidth": 12}},
{
"widgettype": "HBox",
"options": {"spacing": "4px"},
"subwidgets": [
{
"widgettype": "Text",
"options": {"text": "👁", "cfontsize": 14, "css": "clickable", "color": "#4a90d9", "title": "查看"},
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self",
"script": "window.open(" + json.dumps(file_url) + ",'_blank')"}]
},
{
"widgettype": "Text",
"options": {"text": "⬇", "cfontsize": 14, "css": "clickable", "color": "#50b86c", "title": "下载"},
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self",
"script": "var a=document.createElement('a');a.href=" + json.dumps(file_url) + ";a.download='';document.body.appendChild(a);a.click();document.body.removeChild(a)"}]
},
{
"widgettype": "Text",
"options": {"text": "🗑", "cfontsize": 14, "css": "clickable", "color": "#f56c6c", "title": "删除"},
"binds": [{
"wid": "self", "event": "click",
"actiontype": "urlwidget",
"target": "file_list_panel",
"mode": "replace",
"options": {"url": del_url}
}]
}
]
}
]
}
file_widgets.append(row)
result = {
"widgettype": "VBox",
"options": {"width": "100%", "spacing": "0"},
"subwidgets": file_widgets
}
return result