From 12a8caa4632c22cb81b2ac63b73e2eb3bbdbf24e Mon Sep 17 00:00:00 2001 From: root Date: Wed, 7 Oct 2026 01:52:23 +0800 Subject: [PATCH] =?UTF-8?q?copy(server):=20=E6=96=87=E4=BB=B6=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=94=99=E8=AF=AF=E6=96=87=E6=A1=88=E6=8C=89=20human-?= =?UTF-8?q?copy=20=E9=87=8D=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 「根」统一叫登记目录;去掉「请…」;H&R 拦截不再露待处理事项 ID,改给下一步动作。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei --- .../server/src/modules/file-manager/common.ts | 19 ++++++++-------- .../src/modules/file-manager/download.ts | 4 ++-- .../src/modules/file-manager/identify.ts | 6 ++--- apps/server/src/modules/file-manager/list.ts | 2 +- .../server/src/modules/file-manager/mutate.ts | 9 ++++---- .../modules/file-manager/organize-sources.ts | 4 ++-- .../src/modules/file-manager/organize.ts | 20 ++++++++--------- .../src/modules/file-manager/preview.ts | 2 +- .../modules/file-manager/source-monitor.ts | 6 ++--- .../src/modules/file-manager/suggest-name.ts | 2 +- .../src/modules/file-manager/transfer.ts | 11 +++++----- apps/server/src/modules/file-manager/trash.ts | 22 +++++++++---------- .../server/src/modules/file-manager/upload.ts | 3 ++- 13 files changed, 57 insertions(+), 53 deletions(-) diff --git a/apps/server/src/modules/file-manager/common.ts b/apps/server/src/modules/file-manager/common.ts index 177f963a..bdf4a9ed 100644 --- a/apps/server/src/modules/file-manager/common.ts +++ b/apps/server/src/modules/file-manager/common.ts @@ -131,7 +131,8 @@ export function fsErrorToFiles(e0: unknown): FilesError | null { const code = errnoOf(e); if (typeof code !== "string" || !/^E[A-Z]+$/.test(code)) return null; const msg = e instanceof Error ? e.message : String(e); - if (OFFLINE_CODES.has(code)) return new FilesError("root_unavailable", `读取失败(${code}),根可能已离线:${msg}`); + if (OFFLINE_CODES.has(code)) + return new FilesError("root_unavailable", `读取失败(${code}),登记目录可能已离线:${msg}`); if (code === "ENOENT" || code === "ENOTDIR") return new FilesError("not_found", `路径已不存在(${code})`); if (code === "EEXIST") return new FilesError("exists", `目标已存在:${msg}`); if (code === "EACCES" || code === "EPERM") return new FilesError("forbidden", `没有访问权限(${code})`); @@ -225,7 +226,7 @@ export async function loadRootDefs(exec: Exec): Promise { export async function rootDef(exec: Exec, id: string): Promise { const r = (await loadRootDefs(exec)).find((x) => x.id === id); - if (!r) throw new FilesError("not_found", `根不存在或未启用:${id}`); + if (!r) throw new FilesError("not_found", `登记目录不存在或未启用:${id}`); return r; } @@ -278,7 +279,7 @@ export async function probeRoot(def: RootDef, timeoutMs: number): Promise { const h = await probeRoot(def, d.options.healthTimeoutMs); if (h.health !== "healthy" || !h.real) - throw new FilesError("root_unavailable", `根 ${def.name} 不可用(${h.reason ?? h.health})`); + throw new FilesError("root_unavailable", `登记目录「${def.name}」不可用(${h.reason ?? h.health})`); return h.real; } @@ -331,16 +332,16 @@ export async function resolveIn( try { parentReal = await realpath(dirname(joined)); } catch (e) { - if (isOfflineErr(e)) throw new FilesError("root_unavailable", `根 ${def.name} 读取失败(${errnoOf(e)})`); + if (isOfflineErr(e)) throw new FilesError("root_unavailable", `登记目录「${def.name}」读取失败(${errnoOf(e)})`); throw new FilesError("not_found", `父目录不存在:${dirname(rel) || "/"}`); } - if (!within(parentReal, rootReal)) throw new FilesError("path_escape", `路径经符号链接指向根外:${rel}`); + if (!within(parentReal, rootReal)) throw new FilesError("path_escape", `路径经符号链接指向登记目录之外:${rel}`); const abs = join(parentReal, basename(joined)); let lst: Awaited> | null = null; try { lst = await lstat(abs); } catch (e) { - if (isOfflineErr(e)) throw new FilesError("root_unavailable", `根 ${def.name} 读取失败(${errnoOf(e)})`); + if (isOfflineErr(e)) throw new FilesError("root_unavailable", `登记目录「${def.name}」读取失败(${errnoOf(e)})`); if (errnoOf(e) !== "ENOENT") throw new FilesError("bad_request", `无法访问 ${rel}: ${errnoOf(e)}`); } if (!lst && opts.mustExist !== false) throw new FilesError("not_found", `不存在:${rel}`); @@ -348,7 +349,7 @@ export async function resolveIn( if (isSymlink) { const target = await realpath(abs).catch(() => null); if (!target || !within(target, rootReal)) - throw new FilesError("path_escape", `符号链接指向根外或无法解析,不能操作:${rel}`); + throw new FilesError("path_escape", `符号链接指向登记目录之外或无法解析,不能操作:${rel}`); } return { def, rootReal, rel: relOf(rootReal, abs), abs, lst, isSymlink }; } @@ -382,7 +383,7 @@ export async function recheck(abs: string, rootReal: string): Promise { if (isRemotePath(abs)) { try { const r = await vrealpath(abs); - if (!within(r, rootReal)) throw new FilesError("path_escape", `执行时路径指向根外:${abs}`); + if (!within(r, rootReal)) throw new FilesError("path_escape", `执行时路径指向登记目录之外:${abs}`); return r; } catch (e) { if (errnoOf(e) === "ENOENT") throw new FilesError("plan_stale", `源已不存在:${abs}`); @@ -396,7 +397,7 @@ export async function recheck(abs: string, rootReal: string): Promise { if (isOfflineErr(e)) throw new FilesError("root_unavailable", `读取失败(${errnoOf(e)})`); throw new FilesError("plan_stale", `源已不存在:${abs}`); } - if (!within(r, rootReal)) throw new FilesError("path_escape", `执行时路径指向根外:${abs}`); + if (!within(r, rootReal)) throw new FilesError("path_escape", `执行时路径指向登记目录之外:${abs}`); return r; } diff --git a/apps/server/src/modules/file-manager/download.ts b/apps/server/src/modules/file-manager/download.ts index b9b30bf3..e136ab7c 100644 --- a/apps/server/src/modules/file-manager/download.ts +++ b/apps/server/src/modules/file-manager/download.ts @@ -46,7 +46,7 @@ export async function downloadFile( ): Promise { // 路由中间件已按 auth:"session" 拦截;这里同口径复核(纵深防御) if (!isAdminSession(principal)) - throw new FilesError("forbidden", "下载仅限控制台管理员会话,API Key / DEV key 不可用"); + throw new FilesError("forbidden", "只有控制台管理员会话能下载,API Key / DEV key 不行"); const r = await resolveIn(d, q.root, q.path); if (r.rel === "" || r.lst?.isDirectory()) throw new FilesError("bad_request", "只能下载文件,不能下载目录"); const name = basename(r.abs); @@ -54,7 +54,7 @@ export async function downloadFile( // FM-2:可预览的文本类(txt/log/md/json…)、.torrent、avif/gif 也允许下载 const pk = previewKindOf(name); if (!ALLOWED.has(kind) && !pk) - throw new FilesError("forbidden", `只能下载 NFO、字幕、图片、文本和 .torrent,此文件类型为 ${kind}`); + throw new FilesError("forbidden", `只能下载 NFO、字幕、图片、文本和 .torrent,这个文件是 ${kind}`); const real = await recheck(r.abs, r.rootReal); if (r.def.storageId) return downloadRemote(d, principal, q, r.rel, real, name, kind, pk); const file = await Deno.open(real, { read: true }); diff --git a/apps/server/src/modules/file-manager/identify.ts b/apps/server/src/modules/file-manager/identify.ts index 186d4baf..f13f6ac5 100644 --- a/apps/server/src/modules/file-manager/identify.ts +++ b/apps/server/src/modules/file-manager/identify.ts @@ -305,7 +305,7 @@ export async function identifyCore( if (remoteQueries >= REMOTE_QUERY_MAX) { res = { list: [], - warning: `远端查询已达上限 ${REMOTE_QUERY_MAX} 次,此项只查了本地。缩小范围后重新识别`, + warning: `远端查询已达上限 ${REMOTE_QUERY_MAX} 次,这一项只查了本地。缩小范围后重新识别`, }; remoteCache.set(key, res); return res; @@ -318,7 +318,7 @@ export async function identifyCore( return res; } if (opts.signal?.aborted) { - res = { list: [], warning: `远端查询超时(${IDENTIFY_WAIT_MAX_MS / 1000}s),此项只查了本地` }; + res = { list: [], warning: `远端查询超时(${IDENTIFY_WAIT_MAX_MS / 1000}s),这一项只查了本地` }; remoteCache.set(key, res); return res; } @@ -463,7 +463,7 @@ export async function identifyCore( const m = map(parsed); if (m.resolution.method === "unresolved") { unresolved = true; - warnings.push("集数对不上作品的季集,需要人工指定"); + warnings.push("集数对不上作品的季集,需要手动指定"); } else shown = m.parsed; } } diff --git a/apps/server/src/modules/file-manager/list.ts b/apps/server/src/modules/file-manager/list.ts index ad668c98..a48bbf17 100644 --- a/apps/server/src/modules/file-manager/list.ts +++ b/apps/server/src/modules/file-manager/list.ts @@ -492,7 +492,7 @@ export async function listDir(d: FmDeps, q: FilesListQuery): Promise { const r = await resolveIn(d, body.root, body.path); - if (!(await isWritableTarget(r.def, r.rootReal))) throw new FilesError("not_writable", `根 ${r.def.name} 不可写`); + if (!(await isWritableTarget(r.def, r.rootReal))) + throw new FilesError("not_writable", `登记目录「${r.def.name}」不可写`); if (!r.lst?.isDirectory() || r.isSymlink) throw new FilesError("bad_request", `不是目录:${r.rel || "/"}`); assertCleanName(body.name); await recheck(r.abs, r.rootReal); @@ -59,8 +60,8 @@ export async function renameEntry( body: FilesRenameRequest, ): Promise { const r = await resolveIn(d, body.root, body.path); - if (r.rel === "") throw new FilesError("bad_request", "根目录不能重命名"); - if (!r.def.modifiable) throw new FilesError("not_writable", `根 ${r.def.name} 只读,不能重命名`); + if (r.rel === "") throw new FilesError("bad_request", "登记目录本身不能重命名"); + if (!r.def.modifiable) throw new FilesError("not_writable", `登记目录「${r.def.name}」只读,不能重命名`); assertCleanName(body.newName); const oldName = basename(r.abs); if (body.newName === oldName) throw new FilesError("bad_request", "新名称与原名称相同"); @@ -71,7 +72,7 @@ export async function renameEntry( if (hits.length > 0) throw new FilesError( "download_active", - `该路径属于下载任务 ${hits.map((h) => h.taskId).join(", ")},重命名会让下载器找不到文件。先整理再改`, + `该路径属于下载任务 ${hits.map((h) => h.taskId).join(", ")},重命名会让下载器找不到文件。整理后再改名`, ); const dirAbs = dirname(r.abs); diff --git a/apps/server/src/modules/file-manager/organize-sources.ts b/apps/server/src/modules/file-manager/organize-sources.ts index 4a82467c..bd62af88 100644 --- a/apps/server/src/modules/file-manager/organize-sources.ts +++ b/apps/server/src/modules/file-manager/organize-sources.ts @@ -70,7 +70,7 @@ async function editSources( export async function saveOrganizeSource(d: FmDeps, id: string, input: Omit) { const source = await resolveIn(d, input.source.root, input.source.path); - if (source.isSymlink || !source.lst?.isDirectory()) throw new FilesError("bad_request", "来源必须为可访问的目录"); + if (source.isSymlink || !source.lst?.isDirectory()) throw new FilesError("bad_request", "来源必须是可访问的目录"); const target = await resolveIn(d, input.target.root, input.target.path); if ( target.isSymlink || @@ -83,7 +83,7 @@ export async function saveOrganizeSource(d: FmDeps, id: string, input: Omit false, ))) ) - throw new FilesError("not_writable", "目标必须为可写的纳管媒体库目录"); + throw new FilesError("not_writable", "目标必须是可写的媒体库目录"); // Alist 来源(对照 MP 远程目录监控):只能手动或目录快照触发,qB 标签对网盘没有意义 if (source.def.storageId && input.trigger === "qb_tag") throw new FilesError("bad_request", "Alist 来源只能用手动或目录快照触发"); diff --git a/apps/server/src/modules/file-manager/organize.ts b/apps/server/src/modules/file-manager/organize.ts index 6a63248e..d5a62354 100644 --- a/apps/server/src/modules/file-manager/organize.ts +++ b/apps/server/src/modules/file-manager/organize.ts @@ -212,7 +212,7 @@ export async function organizeFiles( if (!episodeRange(it.episodes)) throw new FilesError("bad_request", `${it.path}:集号 ${it.episodes.join(",")} 不连续,只支持连续区间`); const r = await resolveIn(d, it.root, it.path); - if (r.lst?.isDirectory()) throw new FilesError("bad_request", `${r.rel}:目录项不能指定集号,请展开后逐个视频指定`); + if (r.lst?.isDirectory()) throw new FilesError("bad_request", `${r.rel}:目录不能指定集号,展开后逐个视频指定`); } // G1:外部候选 → 本地作品(就地建,幂等);每项解析出 mediaEntityId @@ -287,11 +287,11 @@ export async function organizeFiles( const roots = new Set(); for (const it of body.items) { const r = await resolveIn(d, it.root, it.path); - if (r.rel === "") throw new FilesError("bad_request", "不能整理整个根"); + if (r.rel === "") throw new FilesError("bad_request", "不能整理整个登记目录"); if (r.isSymlink) throw new FilesError("bad_request", `不整理符号链接:${r.rel}`); const action = it.operation ?? body.action; if (action === "move" && !r.def.modifiable) - throw new FilesError("not_writable", `源根 ${r.def.name} 只读,不能移动`); + throw new FilesError("not_writable", `登记目录「${r.def.name}」只读,不能移动`); roots.add(it.root); const isDir = !!r.lst?.isDirectory(); const entityId = entityOf(it); @@ -306,7 +306,7 @@ export async function organizeFiles( if (action === "in_place") { // 本次保留实际库根与路径,不使用来源配置的搬运目标。 if (!r.def.libraryRootId) - throw new FilesError("bad_request", "原地整理需要文件位于已登记的媒体库目录;请先在设置中登记该目录"); + throw new FilesError("bad_request", "原地整理只能用于媒体库目录里的文件。先在设置里登记这个目录"); targetLib = r.def.libraryRootId; } if (action !== "in_place" && it.targetDir) { @@ -314,19 +314,19 @@ export async function organizeFiles( if (!t.lst?.isDirectory() || t.isSymlink || !t.def.libraryRootId) throw new FilesError("bad_request", `目标目录必须是媒体库目录下已存在的目录:${it.targetDir.path || "/"}`); if (!(await isWritableTarget(t.def, t.rootReal))) - throw new FilesError("not_writable", `目标根 ${t.def.name} 不可写`); + throw new FilesError("not_writable", `登记目录「${t.def.name}」不可写`); targetDir = join(t.def.hostPath, t.rel); targetLib = t.def.libraryRootId; } // 季/集 if (it.season !== undefined || it.episodes !== undefined) { - if (!isTv) warnings.push(`${r.rel}:电影不适用季/集覆写,已忽略`); + if (!isTv) warnings.push(`${r.rel}:电影不用指定季集,已忽略`); else { if (it.season !== undefined) ov.season = it.season; if (it.episodes?.length) { const range = episodeRange(it.episodes); if (!range) throw new FilesError("bad_request", `${r.rel}:集号不连续,只支持连续区间`); - if (isDir) throw new FilesError("bad_request", `${r.rel}:目录项不能指定集号,请展开后逐个视频指定`); + if (isDir) throw new FilesError("bad_request", `${r.rel}:目录不能指定集号,展开后逐个视频指定`); ov.episodes = range; } } @@ -348,7 +348,7 @@ export async function organizeFiles( warnings.push(`${r.rel}:收录版本 ${slotId} 不属于该作品,未应用`); slotId = null; } else if (BUSY_SLOT.has(s.status)) { - warnings.push(`${r.rel}:收录版本正在获取中(${s.status}),不改其状态`); + warnings.push(`${r.rel}:收录版本正在下载(${s.status}),不改其状态`); slotId = null; } } @@ -550,7 +550,7 @@ export async function organizeFiles( if (block) throw new FilesError( "download_active", - `下载任务 ${block.downloadTaskId} 的做种义务${block.hnr === "unknown" ? "未知" : "未满足"},不能移动,也不会自动改为复制。已建待处理事项 ${block.decisionId}`, + `下载任务 ${block.downloadTaskId} 的做种义务${block.hnr === "unknown" ? "未知" : "未满足"},不能移动,也不会自动改为复制。改用复制,或等做种完成后再移动`, ); } // 同时设为作品的季集结构:所有执行前检查(计划一致、Move 授权、H&R)通过后才切换作品 @@ -692,6 +692,6 @@ async function pickPlan(d: FmDeps, planIds: string[], reqHash: string): Promise< .where("title", "=", "files_plan") .execute(); const hit = rows.find((r) => (r.content as { op?: string; requestHash?: string }).requestHash === reqHash); - if (!hit) throw new FilesError("plan_stale", "没有与该组请求一致的 planId,重新预览"); + if (!hit) throw new FilesError("plan_stale", "没有与这组请求一致的 planId,重新预览"); return hit.id; } diff --git a/apps/server/src/modules/file-manager/preview.ts b/apps/server/src/modules/file-manager/preview.ts index 2e084d1c..08fce832 100644 --- a/apps/server/src/modules/file-manager/preview.ts +++ b/apps/server/src/modules/file-manager/preview.ts @@ -54,7 +54,7 @@ export async function previewFile( principal: Principal | undefined, q: FilesPreviewQuery, ): Promise { - if (!isAdminSession(principal)) throw new FilesError("forbidden", "预览仅限控制台管理员会话"); + if (!isAdminSession(principal)) throw new FilesError("forbidden", "只有控制台管理员会话能预览"); const r = await resolveIn(d, q.root, q.path); if (r.rel === "" || r.lst?.isDirectory()) throw new FilesError("bad_request", "只能预览文件,不能预览目录"); const name = basename(r.abs); diff --git a/apps/server/src/modules/file-manager/source-monitor.ts b/apps/server/src/modules/file-manager/source-monitor.ts index a36fa6cc..cc1ee2f5 100644 --- a/apps/server/src/modules/file-manager/source-monitor.ts +++ b/apps/server/src/modules/file-manager/source-monitor.ts @@ -58,7 +58,7 @@ export async function walkSource( if (!within(path, config.source.path) && config.source.path !== "") throw new FilesError("path_escape", "扫描路径必须在来源目录内"); const start = await resolveIn(d, config.source.root, path); - if (start.isSymlink || !start.lst?.isDirectory()) throw new FilesError("bad_request", "来源必须为目录"); + if (start.isSymlink || !start.lst?.isDirectory()) throw new FilesError("bad_request", "来源必须是目录"); if (start.def.storageId) return walkRemoteSource(start, prev); const files: Record> = {}; const stack = [start.abs]; @@ -286,7 +286,7 @@ export async function scanOrganizeSource(d: FmDeps, input: { sourceId: string }) files: row?.baseline_at ? ((row.snapshot as unknown as Snapshot).files ?? {}) : {}, since: row?.baseline_at && row.last_scan_at ? new Date(row.last_scan_at).getTime() : null, }); - if (scan.truncated) throw new FilesError("too_large", "来源超过单次扫描上限,请选择更小的目录"); + if (scan.truncated) throw new FilesError("too_large", "来源超过单次扫描上限,选一个更小的目录"); const baseline = !row?.baseline_at; const advance = advanceSnapshot(baseline ? null : (row!.snapshot as unknown as Snapshot), scan.files); let queued = 0; @@ -334,7 +334,7 @@ export async function scanOrganizeSource(d: FmDeps, input: { sourceId: string }) if (!existing) await upsertDecision(d.ao, trx, { kind: "identity_ambiguous", - title: `整理来源待识别:${basename(path)}`, + title: `整理来源有未识别的文件:${basename(path)}`, subjectType: "organize_source", subjectId: config.id, dedupeKey, diff --git a/apps/server/src/modules/file-manager/suggest-name.ts b/apps/server/src/modules/file-manager/suggest-name.ts index 2d65e36c..d923fd25 100644 --- a/apps/server/src/modules/file-manager/suggest-name.ts +++ b/apps/server/src/modules/file-manager/suggest-name.ts @@ -29,7 +29,7 @@ function segmentsOf(rel: string) { export async function suggestName(d: FmDeps, body: FilesSuggestNameRequest): Promise { const db = d.ctx.db; const r = await resolveIn(d, body.root, body.path); - if (r.rel === "") throw new FilesError("bad_request", "不能给登记目录本身生成推荐名"); + if (r.rel === "") throw new FilesError("bad_request", "登记目录本身不能生成推荐名"); const isDir = !!r.lst?.isDirectory(); const parsed = parseReleasePath(segmentsOf(r.rel)); diff --git a/apps/server/src/modules/file-manager/transfer.ts b/apps/server/src/modules/file-manager/transfer.ts index 04a8826a..9a07cc75 100644 --- a/apps/server/src/modules/file-manager/transfer.ts +++ b/apps/server/src/modules/file-manager/transfer.ts @@ -97,13 +97,14 @@ export async function transfer( // 目标:可写的媒体库目录(本地或 Alist),或 Alist 存储根(文件管理的 st: 根,对照 MP 存储内移动 / 复制) const storageTarget = t.def.kind === "storage"; if ((!t.def.libraryRootId && !storageTarget) || !(await isWritableTarget(t.def, t.rootReal))) - throw new FilesError("not_writable", `目标根 ${t.def.name} 不可写,只能选 可写的媒体库目录或 Alist 存储`); + throw new FilesError("not_writable", `登记目录「${t.def.name}」不可写,只能选可写的媒体库目录或 Alist 存储`); const sources: Resolved[] = []; for (const it of body.items) { const s = await resolveIn(d, it.root, it.path); - if (s.rel === "") throw new FilesError("bad_request", "根目录不能移动或复制"); - if (mode === "move" && !s.def.modifiable) throw new FilesError("not_writable", `源根 ${s.def.name} 只读,只能复制`); + if (s.rel === "") throw new FilesError("bad_request", "登记目录本身不能移动或复制"); + if (mode === "move" && !s.def.modifiable) + throw new FilesError("not_writable", `登记目录「${s.def.name}」只读,只能复制`); if (within(t.abs, s.abs)) throw new FilesError("bad_request", `目标在源目录之内:${s.rel}`); if (dirname(s.abs) === t.abs) throw new FilesError("bad_request", `源已在目标目录中:${s.rel}`); if (s.isSymlink) throw new FilesError("bad_request", `不能移动符号链接:${s.rel}`); @@ -169,7 +170,7 @@ export async function transfer( if (storageTarget && mode === "move" && registered) throw new FilesError( "needs_retire", - `${s.rel} 含已整理的文件,只能移动到媒体库目录;要放到 Alist 存储请用复制或先从媒体库移除`, + `${s.rel} 含已整理的文件,只能移动到媒体库目录;要放到 Alist 存储,改用复制或先从媒体库移除`, ); pairs.push({ source: { root: body.items[i]!.root, path: s.rel }, @@ -200,7 +201,7 @@ export async function transfer( !allowCross && pairs.some((p, i) => p.crossDevice && !isRemotePath(sources[i]!.abs) && !isRemotePath(t.abs)); const warnings: string[] = []; - if (crossBlocked) warnings.push("未开启跨卷移动,执行会失败。改用复制,或在 设置 → 整理 中开启"); + if (crossBlocked) warnings.push("未开启跨卷移动,执行会失败。改用复制,或在「设置 → 整理」里开启"); if (conflictSrc.size > 0) warnings.push(`${conflictSrc.size} 个文件在目标已存在,不覆盖,执行时记为失败`); if (truncated) warnings.push("源文件数超过上限,分批操作"); const base = { mode, pairs, totalBytes, freeBytes, spaceOk, hnrBlocks, truncated, warnings }; diff --git a/apps/server/src/modules/file-manager/trash.ts b/apps/server/src/modules/file-manager/trash.ts index 2c043854..cbc51767 100644 --- a/apps/server/src/modules/file-manager/trash.ts +++ b/apps/server/src/modules/file-manager/trash.ts @@ -192,12 +192,12 @@ const BLOCK_ERR: Record< [ConstructorParameters[0], string] > = { download_active: ["download_active", "属于未完成或做种义务未满足的下载任务"], - is_root: ["bad_request", "不能删除根目录或只读根下的内容"], + is_root: ["bad_request", "不能删除登记目录本身或只读目录里的内容"], needs_retire: ["needs_retire", "含已整理文件,需勾选「同时从媒体库移除」"], - needs_unbind: ["needs_unbind", "是作品/季的绑定目录,需勾选「解除绑定」"], - root_unavailable: ["root_unavailable", "根不可用"], + needs_unbind: ["needs_unbind", "是作品或季的绑定目录,需勾选「解除绑定」"], + root_unavailable: ["root_unavailable", "登记目录不可用"], not_found: ["not_found", "不存在"], - escape: ["path_escape", "路径指向根外"], + escape: ["path_escape", "路径指向登记目录之外"], }; export async function deleteFiles( @@ -245,13 +245,13 @@ export async function deleteFiles( if (body.planId) planId = (await loadPlan(d, body.planId, "delete", requestHash)).id; if (!isAdmin(principal)) { // API Key:每次删除都要一次性审批,审批绑定 dryRun 的计划 id + 内容哈希 - if (!planId) throw new FilesError("approval_required", "API Key 删除需先 dryRun 取得 planId 并获得一次性审批"); + if (!planId) throw new FilesError("approval_required", "用 API Key 删除要先 dryRun 拿到 planId,再申请一次性审批"); const plan = await loadPlan(d, planId, "delete", requestHash); const ok = await consumeApproval(d, plan.id, plan.contentHash); if (!ok) throw new FilesError("approval_required", `删除需要一次性审批(planType=files_plan, planId=${plan.id})`); } if (body.mode === "permanent" && body.items.length === 1 && body.confirmName !== basename(checks[0]!.path)) - throw new FilesError("bad_request", "永久删除需在 confirmName 中输入文件名确认"); + throw new FilesError("bad_request", "永久删除要在 confirmName 里填文件名确认"); if (planId) await retirePlan(d, planId); const now = d.ao.ports.now(); @@ -314,7 +314,7 @@ export async function deleteFiles( await upsertDecision(d.ao, trx, { kind: "other", title: `收录版本的文件已删除:${s.name}`, - summary: "满足该收录版本的文件已在文件管理中删除,收录版本改为需关注。确认是否重新获取", + summary: "这个收录版本的文件已在文件管理里删除,收录版本改为需关注。确认是否重新下载", subjectType: "acquisition_slot", subjectId: s.id, dedupeKey: `files_delete_slot:${s.id}`, @@ -553,7 +553,7 @@ export async function mkdirWithin(rootReal: string, rel: string): Promise throw new FilesError("path_escape", `还原路径中的 ${relOfRoot(rootReal, cur)} 是符号链接,不能还原`); if (st && !st.isDirectory()) throw new FilesError("exists", `还原路径中的 ${relOfRoot(rootReal, cur)} 不是目录`); if (!st) await mkdir(cur); - if (!within(await realpath(cur), rootReal)) throw new FilesError("path_escape", `还原路径指向根外:${rel}`); + if (!within(await realpath(cur), rootReal)) throw new FilesError("path_escape", `还原路径指向登记目录之外:${rel}`); } } const relOfRoot = (root: string, p: string) => (p === root ? "" : p.slice(root.length + 1)); @@ -579,12 +579,12 @@ export async function restoreTrash( throw new FilesError("bad_request", "还原目标必须是已存在的目录"); // "还原到…"只能进可写的 managed 库根(与 move/copy 的目标规则一致;register_only / 下载根拒绝,验收 07 S-5) if (target && !(await isWritableTarget(target.def, target.rootReal))) - throw new FilesError("not_writable", `还原目标根 ${target.def.name} 不可写,只能选 可写的媒体库目录`); + throw new FilesError("not_writable", `登记目录「${target.def.name}」不可写,只能还原到可写的媒体库目录`); for (const id of body.ids) { try { const row = await d.ctx.db.selectFrom("trash_item").selectAll().where("id", "=", id).executeTakeFirst(); if (!row) throw new FilesError("not_found", "回收站条目不存在"); - if (!trashPathSafe(d, row)) throw new FilesError("path_escape", "回收站路径异常,拒绝操作"); + if (!trashPathSafe(d, row)) throw new FilesError("path_escape", "回收站路径异常,已拒绝"); let rootId = row.root_id; let dst: string; let rootReal: string; @@ -608,7 +608,7 @@ export async function restoreTrash( dst = parent.abs; } if (await (isRemotePath(dst) ? vlstat(dst) : lstat(dst)).catch(() => null)) - throw new FilesError("exists", "原位置已被占用,可选择「还原到…」"); + throw new FilesError("exists", "原位置已被占用,用「还原到…」换个位置"); if ( isRemotePath(dst) !== isRemotePath(row.trash_path) || (isRemotePath(dst) && !sameStorageOf(dst, row.trash_path)) diff --git a/apps/server/src/modules/file-manager/upload.ts b/apps/server/src/modules/file-manager/upload.ts index 2e112111..02916125 100644 --- a/apps/server/src/modules/file-manager/upload.ts +++ b/apps/server/src/modules/file-manager/upload.ts @@ -41,7 +41,8 @@ export async function uploadFile( if (!body) throw new FilesError("bad_request", "请求体为空"); const r = await resolveIn(d, q.root, q.path); if (!r.lst?.isDirectory() || r.isSymlink) throw new FilesError("bad_request", `不是目录:${r.rel || "/"}`); - if (!(await isWritableTarget(r.def, r.rootReal))) throw new FilesError("not_writable", `根 ${r.def.name} 不可写`); + if (!(await isWritableTarget(r.def, r.rootReal))) + throw new FilesError("not_writable", `登记目录「${r.def.name}」不可写`); if (sanitizeSegment(q.name) !== q.name) throw new FilesError("bad_request", `文件名含非法字符或首尾有空白/点,可改为:${sanitizeSegment(q.name)}`); await recheck(r.abs, r.rootReal);