mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-12 10:47:30 +02:00
Add files via upload
This commit is contained in:
7 files changed
+383
-4
No files matched your search
@@ -846,6 +846,7 @@ func setupRoutes(
|
|||||||
protected.GET("/chat-uploads/download", chatUploadsHandler.Download)
|
protected.GET("/chat-uploads/download", chatUploadsHandler.Download)
|
||||||
protected.GET("/chat-uploads/content", chatUploadsHandler.GetContent)
|
protected.GET("/chat-uploads/content", chatUploadsHandler.GetContent)
|
||||||
protected.POST("/chat-uploads", chatUploadsHandler.Upload)
|
protected.POST("/chat-uploads", chatUploadsHandler.Upload)
|
||||||
|
protected.POST("/chat-uploads/mkdir", chatUploadsHandler.Mkdir)
|
||||||
protected.DELETE("/chat-uploads", chatUploadsHandler.Delete)
|
protected.DELETE("/chat-uploads", chatUploadsHandler.Delete)
|
||||||
protected.PUT("/chat-uploads/rename", chatUploadsHandler.Rename)
|
protected.PUT("/chat-uploads/rename", chatUploadsHandler.Rename)
|
||||||
protected.PUT("/chat-uploads/content", chatUploadsHandler.PutContent)
|
protected.PUT("/chat-uploads/content", chatUploadsHandler.PutContent)
|
||||||
|
|||||||
@@ -205,6 +205,77 @@ func (h *ChatUploadsHandler) Delete(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type chatUploadMkdirBody struct {
|
||||||
|
Parent string `json:"parent"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mkdir POST /api/chat-uploads/mkdir — 在 parent 目录下新建子目录(parent 为 chat_uploads 下相对路径,空表示根目录;name 为单段目录名)
|
||||||
|
func (h *ChatUploadsHandler) Mkdir(c *gin.Context) {
|
||||||
|
var body chatUploadMkdirBody
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(body.Name)
|
||||||
|
if name == "" || strings.ContainsAny(name, `/\`) || name == "." || name == ".." {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if utf8.RuneCountInString(name) > 200 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "name too long"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := strings.TrimSpace(body.Parent)
|
||||||
|
parent = filepath.ToSlash(filepath.Clean(filepath.FromSlash(parent)))
|
||||||
|
parent = strings.Trim(parent, "/")
|
||||||
|
if parent == "." {
|
||||||
|
parent = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
root, err := h.absRoot()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if parent != "" {
|
||||||
|
absParent, err := h.resolveUnderChatUploads(parent)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
st, err := os.Stat(absParent)
|
||||||
|
if err != nil || !st.IsDir() {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "parent not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rel string
|
||||||
|
if parent == "" {
|
||||||
|
rel = name
|
||||||
|
} else {
|
||||||
|
rel = parent + "/" + name
|
||||||
|
}
|
||||||
|
absNew, err := h.resolveUnderChatUploads(rel)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(absNew); err == nil {
|
||||||
|
c.JSON(http.StatusConflict, gin.H{"error": "already exists"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := os.Mkdir(absNew, 0755); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
relOut, _ := filepath.Rel(root, absNew)
|
||||||
|
c.JSON(http.StatusOK, gin.H{"ok": true, "relativePath": filepath.ToSlash(relOut)})
|
||||||
|
}
|
||||||
|
|
||||||
type chatUploadRenameBody struct {
|
type chatUploadRenameBody struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
NewName string `json:"newName"`
|
NewName string `json:"newName"`
|
||||||
|
|||||||
@@ -13330,6 +13330,86 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 新建文件夹弹窗:层次清晰、留白舒适,无强装饰 */
|
||||||
|
.chat-files-mkdir-modal-content {
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-mkdir-body {
|
||||||
|
padding: 26px 28px 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-mkdir-location {
|
||||||
|
margin: 0 0 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-mkdir-location-caption {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-mkdir-path-box {
|
||||||
|
padding: 11px 14px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-mkdir-path {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--text-primary);
|
||||||
|
word-break: break-all;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-mkdir-label {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-mkdir-field-name {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-mkdir-field-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-mkdir-input {
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-mkdir-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-mkdir-footer {
|
||||||
|
padding: 18px 28px 22px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-files-toast {
|
.chat-files-toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1100;
|
z-index: 1100;
|
||||||
|
|||||||
@@ -1021,6 +1021,14 @@
|
|||||||
"confirmDeleteFolder": "Delete this folder and everything inside it? This cannot be undone.",
|
"confirmDeleteFolder": "Delete this folder and everything inside it? This cannot be undone.",
|
||||||
"deleteFolderTitle": "Delete folder",
|
"deleteFolderTitle": "Delete folder",
|
||||||
"uploadToFolderTitle": "Upload file into this folder",
|
"uploadToFolderTitle": "Upload file into this folder",
|
||||||
|
"newFolderButton": "New folder",
|
||||||
|
"newFolderTitle": "New folder",
|
||||||
|
"newFolderLocation": "Location",
|
||||||
|
"newFolderNameLabel": "Folder name",
|
||||||
|
"newFolderNamePlaceholder": "Name only, no slashes",
|
||||||
|
"mkdirOk": "Folder created",
|
||||||
|
"mkdirExists": "A file or folder with that name already exists",
|
||||||
|
"mkdirInvalidName": "Invalid name: cannot be empty or contain /, \\, or use . or ..",
|
||||||
"colSubPath": "Subfolder",
|
"colSubPath": "Subfolder",
|
||||||
"folderRoot": "(root)",
|
"folderRoot": "(root)",
|
||||||
"groupCount": "{{count}} files",
|
"groupCount": "{{count}} files",
|
||||||
|
|||||||
@@ -1021,6 +1021,14 @@
|
|||||||
"confirmDeleteFolder": "确定删除该文件夹及其中的全部文件?此操作不可恢复。",
|
"confirmDeleteFolder": "确定删除该文件夹及其中的全部文件?此操作不可恢复。",
|
||||||
"deleteFolderTitle": "删除文件夹",
|
"deleteFolderTitle": "删除文件夹",
|
||||||
"uploadToFolderTitle": "上传文件到此文件夹",
|
"uploadToFolderTitle": "上传文件到此文件夹",
|
||||||
|
"newFolderButton": "新建文件夹",
|
||||||
|
"newFolderTitle": "新建文件夹",
|
||||||
|
"newFolderLocation": "位置",
|
||||||
|
"newFolderNameLabel": "文件夹名称",
|
||||||
|
"newFolderNamePlaceholder": "仅名称,不含 /",
|
||||||
|
"mkdirOk": "文件夹已创建",
|
||||||
|
"mkdirExists": "该名称已存在",
|
||||||
|
"mkdirInvalidName": "名称无效:不能为空,且不能包含 /、\\ 或 . / ..",
|
||||||
"colSubPath": "子路径",
|
"colSubPath": "子路径",
|
||||||
"folderRoot": "(根目录)",
|
"folderRoot": "(根目录)",
|
||||||
"groupCount": "{{count}} 个文件",
|
"groupCount": "{{count}} 个文件",
|
||||||
|
|||||||
+187
-4
@@ -13,6 +13,98 @@ let chatFilesBrowsePath = [];
|
|||||||
/** 非空时,下一次上传文件落到此相对路径(chat_uploads 下目录),如 2026-03-21/uuid/sub */
|
/** 非空时,下一次上传文件落到此相对路径(chat_uploads 下目录),如 2026-03-21/uuid/sub */
|
||||||
let chatFilesPendingUploadDir = '';
|
let chatFilesPendingUploadDir = '';
|
||||||
|
|
||||||
|
/** 仅前端记录的「空目录」键 parentPath('' 表示 chat_uploads 根)-> 子目录名列表,与树合并以便 mkdir 后可见 */
|
||||||
|
const CHAT_FILES_SYNTHETIC_DIRS_KEY = 'csai_chat_files_synthetic_dirs';
|
||||||
|
let chatFilesSyntheticEmptyDirs = {};
|
||||||
|
|
||||||
|
function chatFilesLoadSyntheticDirsFromStorage() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(CHAT_FILES_SYNTHETIC_DIRS_KEY);
|
||||||
|
if (!raw) return;
|
||||||
|
const o = JSON.parse(raw);
|
||||||
|
if (o && typeof o === 'object') {
|
||||||
|
chatFilesSyntheticEmptyDirs = o;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
chatFilesSyntheticEmptyDirs = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function chatFilesRegisterSyntheticEmptyDir(parentSegments, name) {
|
||||||
|
const p = parentSegments.join('/');
|
||||||
|
if (!chatFilesSyntheticEmptyDirs[p]) {
|
||||||
|
chatFilesSyntheticEmptyDirs[p] = [];
|
||||||
|
}
|
||||||
|
const arr = chatFilesSyntheticEmptyDirs[p];
|
||||||
|
if (arr.indexOf(name) === -1) {
|
||||||
|
arr.push(name);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
localStorage.setItem(CHAT_FILES_SYNTHETIC_DIRS_KEY, JSON.stringify(chatFilesSyntheticEmptyDirs));
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function chatFilesRemoveSyntheticDirSubtree(relPathUnderRoot) {
|
||||||
|
const rel = String(relPathUnderRoot || '').replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, '');
|
||||||
|
if (!rel) return;
|
||||||
|
const parts = rel.split('/').filter(function (x) {
|
||||||
|
return x.length > 0;
|
||||||
|
});
|
||||||
|
if (parts.length === 0) return;
|
||||||
|
const leaf = parts[parts.length - 1];
|
||||||
|
const parentKey = parts.slice(0, -1).join('/');
|
||||||
|
const arr = chatFilesSyntheticEmptyDirs[parentKey];
|
||||||
|
if (arr) {
|
||||||
|
const ix = arr.indexOf(leaf);
|
||||||
|
if (ix >= 0) arr.splice(ix, 1);
|
||||||
|
if (arr.length === 0) delete chatFilesSyntheticEmptyDirs[parentKey];
|
||||||
|
}
|
||||||
|
const prefix = rel + '/';
|
||||||
|
let k;
|
||||||
|
for (k in chatFilesSyntheticEmptyDirs) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(chatFilesSyntheticEmptyDirs, k)) continue;
|
||||||
|
if (k === rel || k.indexOf(prefix) === 0) {
|
||||||
|
delete chatFilesSyntheticEmptyDirs[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
localStorage.setItem(CHAT_FILES_SYNTHETIC_DIRS_KEY, JSON.stringify(chatFilesSyntheticEmptyDirs));
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function chatFilesMergeSyntheticDirsIntoTree(root) {
|
||||||
|
function ensurePath(node, segments) {
|
||||||
|
let n = node;
|
||||||
|
let i;
|
||||||
|
for (i = 0; i < segments.length; i++) {
|
||||||
|
const s = segments[i];
|
||||||
|
if (!n.dirs[s]) n.dirs[s] = chatFilesTreeMakeNode();
|
||||||
|
n = n.dirs[s];
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
let k;
|
||||||
|
for (k in chatFilesSyntheticEmptyDirs) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(chatFilesSyntheticEmptyDirs, k)) continue;
|
||||||
|
const names = chatFilesSyntheticEmptyDirs[k];
|
||||||
|
if (!Array.isArray(names)) continue;
|
||||||
|
const segs = k ? k.split('/').filter(function (x) {
|
||||||
|
return x.length > 0;
|
||||||
|
}) : [];
|
||||||
|
const node = ensurePath(root, segs);
|
||||||
|
let ni;
|
||||||
|
for (ni = 0; ni < names.length; ni++) {
|
||||||
|
const nm = names[ni];
|
||||||
|
if (!nm || typeof nm !== 'string') continue;
|
||||||
|
if (!node.dirs[nm]) node.dirs[nm] = chatFilesTreeMakeNode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function chatFilesLoadBrowsePathFromStorage() {
|
function chatFilesLoadBrowsePathFromStorage() {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(CHAT_FILES_BROWSE_PATH_KEY);
|
const raw = localStorage.getItem(CHAT_FILES_BROWSE_PATH_KEY);
|
||||||
@@ -63,6 +155,7 @@ function chatFilesNormalizeBrowsePathForTree(root) {
|
|||||||
|
|
||||||
function initChatFilesPage() {
|
function initChatFilesPage() {
|
||||||
chatFilesLoadBrowsePathFromStorage();
|
chatFilesLoadBrowsePathFromStorage();
|
||||||
|
chatFilesLoadSyntheticDirsFromStorage();
|
||||||
ensureChatFilesDocClickClose();
|
ensureChatFilesDocClickClose();
|
||||||
const sel = document.getElementById('chat-files-group-by');
|
const sel = document.getElementById('chat-files-group-by');
|
||||||
if (sel) {
|
if (sel) {
|
||||||
@@ -368,6 +461,12 @@ function chatFilesBuildTree(files) {
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chatFilesTreeRootMerged() {
|
||||||
|
const root = chatFilesBuildTree(chatFilesDisplayed);
|
||||||
|
chatFilesMergeSyntheticDirsIntoTree(root);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
function chatFilesTreeNodeMaxMod(node) {
|
function chatFilesTreeNodeMaxMod(node) {
|
||||||
let m = 0;
|
let m = 0;
|
||||||
let i;
|
let i;
|
||||||
@@ -570,7 +669,7 @@ function renderChatFilesTable() {
|
|||||||
let innerHtml;
|
let innerHtml;
|
||||||
|
|
||||||
if (groupMode === 'folder') {
|
if (groupMode === 'folder') {
|
||||||
const root = chatFilesBuildTree(chatFilesDisplayed);
|
const root = chatFilesTreeRootMerged();
|
||||||
chatFilesNormalizeBrowsePathForTree(root);
|
chatFilesNormalizeBrowsePathForTree(root);
|
||||||
const node = chatFilesResolveTreeNode(root, chatFilesBrowsePath);
|
const node = chatFilesResolveTreeNode(root, chatFilesBrowsePath);
|
||||||
const current = node || root;
|
const current = node || root;
|
||||||
@@ -582,6 +681,7 @@ function renderChatFilesTable() {
|
|||||||
|
|
||||||
const tRoot = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.browseRoot') : 'chat_uploads');
|
const tRoot = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.browseRoot') : 'chat_uploads');
|
||||||
const tUp = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.browseUp') : '上级');
|
const tUp = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.browseUp') : '上级');
|
||||||
|
const tMkdir = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.newFolderButton') : '新建文件夹');
|
||||||
const tEmpty = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.folderEmpty') : '此文件夹为空');
|
const tEmpty = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.folderEmpty') : '此文件夹为空');
|
||||||
const tCopyFolder = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.copyFolderPathTitle') : '复制 chat_uploads 下相对路径');
|
const tCopyFolder = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.copyFolderPathTitle') : '复制 chat_uploads 下相对路径');
|
||||||
const tEnter = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.enterFolderTitle') : '进入');
|
const tEnter = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.enterFolderTitle') : '进入');
|
||||||
@@ -603,6 +703,7 @@ function renderChatFilesTable() {
|
|||||||
|
|
||||||
const upDisabled = chatFilesBrowsePath.length === 0 ? ' disabled' : '';
|
const upDisabled = chatFilesBrowsePath.length === 0 ? ' disabled' : '';
|
||||||
const toolbarHtml = '<div class="chat-files-browse-toolbar">' + breadcrumbHtml +
|
const toolbarHtml = '<div class="chat-files-browse-toolbar">' + breadcrumbHtml +
|
||||||
|
'<button type="button" class="btn-secondary chat-files-mkdir-btn" onclick="openChatFilesMkdirModal()">' + tMkdir + '</button>' +
|
||||||
'<button type="button" class="btn-secondary chat-files-browse-up"' + upDisabled + ' onclick="chatFilesNavigateUp()">' + tUp + '</button></div>';
|
'<button type="button" class="btn-secondary chat-files-browse-up"' + upDisabled + ' onclick="chatFilesNavigateUp()">' + tUp + '</button></div>';
|
||||||
|
|
||||||
const svgTrash = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
|
const svgTrash = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
|
||||||
@@ -735,7 +836,7 @@ function renderChatFilesTable() {
|
|||||||
window.chatFilesGroupByChange = chatFilesGroupByChange;
|
window.chatFilesGroupByChange = chatFilesGroupByChange;
|
||||||
|
|
||||||
function chatFilesNavigateInto(name) {
|
function chatFilesNavigateInto(name) {
|
||||||
const root = chatFilesBuildTree(chatFilesDisplayed);
|
const root = chatFilesTreeRootMerged();
|
||||||
chatFilesNormalizeBrowsePathForTree(root);
|
chatFilesNormalizeBrowsePathForTree(root);
|
||||||
const next = chatFilesBrowsePath.concat([name]);
|
const next = chatFilesBrowsePath.concat([name]);
|
||||||
if (!chatFilesResolveTreeNode(root, next)) return;
|
if (!chatFilesResolveTreeNode(root, next)) return;
|
||||||
@@ -744,7 +845,7 @@ function chatFilesNavigateInto(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function chatFilesNavigateBreadcrumb(level) {
|
function chatFilesNavigateBreadcrumb(level) {
|
||||||
const root = chatFilesBuildTree(chatFilesDisplayed);
|
const root = chatFilesTreeRootMerged();
|
||||||
chatFilesNormalizeBrowsePathForTree(root);
|
chatFilesNormalizeBrowsePathForTree(root);
|
||||||
if (level < 0) {
|
if (level < 0) {
|
||||||
chatFilesSetBrowsePath([]);
|
chatFilesSetBrowsePath([]);
|
||||||
@@ -805,6 +906,7 @@ async function deleteChatFolderFromBrowse(folderName) {
|
|||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(await res.text());
|
throw new Error(await res.text());
|
||||||
}
|
}
|
||||||
|
chatFilesRemoveSyntheticDirSubtree(rel);
|
||||||
loadChatFilesPage();
|
loadChatFilesPage();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert((e && e.message) ? e.message : String(e));
|
alert((e && e.message) ? e.message : String(e));
|
||||||
@@ -842,6 +944,9 @@ window.chatFilesCopyFolderPathFromBtn = chatFilesCopyFolderPathFromBtn;
|
|||||||
window.chatFilesDeleteFolderFromBtn = chatFilesDeleteFolderFromBtn;
|
window.chatFilesDeleteFolderFromBtn = chatFilesDeleteFolderFromBtn;
|
||||||
window.chatFilesOpenUploadPicker = chatFilesOpenUploadPicker;
|
window.chatFilesOpenUploadPicker = chatFilesOpenUploadPicker;
|
||||||
window.chatFilesUploadToFolderClick = chatFilesUploadToFolderClick;
|
window.chatFilesUploadToFolderClick = chatFilesUploadToFolderClick;
|
||||||
|
window.openChatFilesMkdirModal = openChatFilesMkdirModal;
|
||||||
|
window.closeChatFilesMkdirModal = closeChatFilesMkdirModal;
|
||||||
|
window.submitChatFilesMkdir = submitChatFilesMkdir;
|
||||||
|
|
||||||
function openChatFilesConversationIdx(idx) {
|
function openChatFilesConversationIdx(idx) {
|
||||||
const f = chatFilesDisplayed[idx];
|
const f = chatFilesDisplayed[idx];
|
||||||
@@ -1018,8 +1123,86 @@ async function submitChatFilesRename() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openChatFilesMkdirModal() {
|
||||||
|
if (chatFilesGetGroupByMode() !== 'folder') return;
|
||||||
|
const hint = document.getElementById('chat-files-mkdir-parent-hint');
|
||||||
|
const input = document.getElementById('chat-files-mkdir-input');
|
||||||
|
const modal = document.getElementById('chat-files-mkdir-modal');
|
||||||
|
const p = chatFilesBrowsePath.join('/');
|
||||||
|
if (hint) hint.textContent = p ? ('chat_uploads/' + p) : 'chat_uploads';
|
||||||
|
if (input) input.value = '';
|
||||||
|
if (modal) modal.style.display = 'block';
|
||||||
|
if (modal && typeof window.applyTranslations === 'function') {
|
||||||
|
window.applyTranslations(modal);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
if (input) input.focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeChatFilesMkdirModal() {
|
||||||
|
const modal = document.getElementById('chat-files-mkdir-modal');
|
||||||
|
if (modal) modal.style.display = 'none';
|
||||||
|
const input = document.getElementById('chat-files-mkdir-input');
|
||||||
|
if (input) input.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitChatFilesMkdir() {
|
||||||
|
const input = document.getElementById('chat-files-mkdir-input');
|
||||||
|
const name = input ? String(input.value).trim() : '';
|
||||||
|
if (!name) {
|
||||||
|
closeChatFilesMkdirModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name.includes('/') || name.includes('\\') || name === '.' || name === '..') {
|
||||||
|
const msg = (typeof window.t === 'function')
|
||||||
|
? window.t('chatFilesPage.mkdirInvalidName')
|
||||||
|
: '名称无效';
|
||||||
|
alert(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parent = chatFilesBrowsePath.join('/');
|
||||||
|
try {
|
||||||
|
const res = await apiFetch('/api/chat-uploads/mkdir', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ parent: parent, name: name })
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
let errText = '';
|
||||||
|
try {
|
||||||
|
const j = await res.json();
|
||||||
|
errText = j.error || JSON.stringify(j);
|
||||||
|
} catch (e2) {
|
||||||
|
errText = await res.text();
|
||||||
|
}
|
||||||
|
if (res.status === 409) {
|
||||||
|
const msg = (typeof window.t === 'function')
|
||||||
|
? window.t('chatFilesPage.mkdirExists')
|
||||||
|
: errText;
|
||||||
|
alert(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(errText || String(res.status));
|
||||||
|
}
|
||||||
|
chatFilesRegisterSyntheticEmptyDir(chatFilesBrowsePath.slice(), name);
|
||||||
|
closeChatFilesMkdirModal();
|
||||||
|
loadChatFilesPage();
|
||||||
|
const okMsg = (typeof window.t === 'function')
|
||||||
|
? window.t('chatFilesPage.mkdirOk')
|
||||||
|
: '文件夹已创建';
|
||||||
|
chatFilesShowToast(okMsg);
|
||||||
|
} catch (e) {
|
||||||
|
alert((e && e.message) ? e.message : String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function chatFilesOpenUploadPicker() {
|
function chatFilesOpenUploadPicker() {
|
||||||
chatFilesPendingUploadDir = '';
|
if (chatFilesGetGroupByMode() === 'folder') {
|
||||||
|
chatFilesPendingUploadDir = chatFilesBrowsePath.join('/');
|
||||||
|
} else {
|
||||||
|
chatFilesPendingUploadDir = '';
|
||||||
|
}
|
||||||
const inp = document.getElementById('chat-files-upload-input');
|
const inp = document.getElementById('chat-files-upload-input');
|
||||||
if (inp) inp.click();
|
if (inp) inp.click();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1818,6 +1818,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="chat-files-mkdir-modal" class="modal">
|
||||||
|
<div class="modal-content chat-files-mkdir-modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 data-i18n="chatFilesPage.newFolderTitle">新建文件夹</h2>
|
||||||
|
<span class="modal-close" onclick="closeChatFilesMkdirModal()">×</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body chat-files-mkdir-body">
|
||||||
|
<div class="chat-files-mkdir-location" aria-live="polite">
|
||||||
|
<div class="chat-files-mkdir-location-caption" data-i18n="chatFilesPage.newFolderLocation">位置</div>
|
||||||
|
<div class="chat-files-mkdir-path-box">
|
||||||
|
<code class="chat-files-mkdir-path" id="chat-files-mkdir-parent-hint">chat_uploads</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="chat-files-rename-label chat-files-mkdir-label">
|
||||||
|
<span class="chat-files-mkdir-field-name">
|
||||||
|
<svg class="chat-files-mkdir-field-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||||||
|
<span data-i18n="chatFilesPage.newFolderNameLabel">文件夹名称</span>
|
||||||
|
</span>
|
||||||
|
<input type="text" id="chat-files-mkdir-input" class="form-control chat-files-mkdir-input" data-i18n="chatFilesPage.newFolderNamePlaceholder" data-i18n-attr="placeholder" placeholder="仅名称,不含 /" autocomplete="off" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer chat-files-mkdir-footer">
|
||||||
|
<button type="button" class="btn-secondary chat-files-mkdir-btn-cancel" onclick="closeChatFilesMkdirModal()" data-i18n="common.cancel">取消</button>
|
||||||
|
<button type="button" class="btn-primary chat-files-mkdir-btn-submit" onclick="submitChatFilesMkdir()" data-i18n="common.ok">确定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Marked.js for Markdown parsing -->
|
<!-- Marked.js for Markdown parsing -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
|
||||||
<!-- DOMPurify for HTML sanitization to prevent XSS -->
|
<!-- DOMPurify for HTML sanitization to prevent XSS -->
|
||||||
|
|||||||
Reference in new issue
Block a user