feat(subtitles): 字幕站(SubHD、字幕库)在线搜索与下载;rar / 7z 解压;验证码与页面解析在沙箱里

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-03 02:28:29 +08:00
1 parent 92d456d123
commit 59b3680f35
32 files changed
+1704 -166

No files matched your search

+33 -13
View File
@@ -3,8 +3,10 @@
// 输入输出必须能 JSON 化(二进制用 base64)。新增任务:在这里加一项,调用方用 runSandbox(名字, 输入)。
import { parseTorrent } from "@umt/adapters";
import { decodeSubtitle, detectSubtitleLanguage, subtitleEpisode, subtitleExtOf } from "@umt/domain";
import { unzipSync } from "fflate";
import { buildPreview } from "../modules/file-manager/preview-build.ts";
import { PAGE_PARSERS, type PageKind } from "../modules/subtitles/online/parse.ts";
import { archiveKindOf, extractArchive, prepareArchives } from "./archives.ts";
import { solveSubhdCaptcha, solveZimukuCaptcha } from "./captcha.ts";
const MAX_SUBTITLE_BYTES = 10 * 1024 * 1024;
const MAX_ARCHIVE_ENTRIES = 300;
@@ -30,8 +32,13 @@ export interface ParsedSubtitleFile {
content: string;
}
/** 字幕 / zip:解压、解码转 UTF-8、识别语言与季集 */
function subtitleParse(input: { files: { name: string; base64: string }[] }) {
/** 子进程撤销权限前调用:预热要读文件的 WASM 库 */
export async function prepareJobs(): Promise<void> {
await prepareArchives();
}
/** 字幕 / zip / rar / 7z:解压、解码转 UTF-8、识别语言与季集 */
async function subtitleParse(input: { files: { name: string; base64: string }[] }) {
const items: ParsedSubtitleFile[] = [];
const skipped: { name: string; reason: string }[] = [];
const one = (name: string, bytes: Uint8Array, archive: string | null) => {
@@ -61,17 +68,16 @@ function subtitleParse(input: { files: { name: string; base64: string }[] }) {
skipped.push({ name: f.name, reason: "内容不是有效的 base64" });
continue;
}
if (/\.zip$/i.test(f.name)) {
const kind = archiveKindOf(f.name);
if (kind) {
let entries: Record<string, Uint8Array>;
try {
let n = 0;
entries = unzipSync(bytes, {
filter: (e) =>
!e.name.endsWith("/") &&
!!subtitleExtOf(e.name) &&
e.originalSize <= MAX_SUBTITLE_BYTES &&
++n <= MAX_ARCHIVE_ENTRIES,
});
entries = await extractArchive(
kind,
bytes,
(name, size) => !!subtitleExtOf(name) && size <= MAX_SUBTITLE_BYTES && ++n <= MAX_ARCHIVE_ENTRIES,
);
} catch {
skipped.push({ name: f.name, reason: "压缩包损坏或加密" });
continue;
@@ -79,8 +85,6 @@ function subtitleParse(input: { files: { name: string; base64: string }[] }) {
const names = Object.keys(entries);
if (!names.length) skipped.push({ name: f.name, reason: "压缩包里没有字幕文件" });
for (const n of names) one(n, entries[n]!, f.name);
} else if (/\.(rar|7z)$/i.test(f.name)) {
skipped.push({ name: f.name, reason: "暂不支持 rar / 7z,请解压后上传或改用 zip" });
} else one(f.name, bytes, null);
}
return { items, skipped };
@@ -103,10 +107,26 @@ function selfTest(input: { mode: "ok" | "hang" | "memory" | "throw" | "io" }) {
return { ok: true };
}
/** SubHD SVG / 字幕库 BMP 验证码 */
function captchaSolve(input: { kind: "subhd"; svg: string } | { kind: "zimuku"; bmpBase64: string }) {
return {
code: input.kind === "subhd" ? solveSubhdCaptcha(input.svg) : solveZimukuCaptcha(b64decode(input.bmpBase64)),
};
}
/** 字幕站页面 → 结构化数据(网页同样不可信) */
function subtitlePage(input: { kind: PageKind; html: string }) {
const fn = PAGE_PARSERS[input.kind] as (html: string) => unknown;
if (!fn) throw new Error(`没有这种页面解析:${input.kind}`);
return fn(input.html);
}
export const SANDBOX_JOBS = {
"subtitle.parse": subtitleParse,
"torrent.parse": torrentParse,
"file.preview": buildPreview,
"captcha.solve": captchaSolve,
"subtitle.page": subtitlePage,
"self.test": selfTest,
} as const;