- 新增 dyEmoji 表情库与解析测试、EmojiPicker 选择器与 EmojiImg 组件 - Markdown 编辑器/渲染支持表情图片,新增 public/emoji 资源与生成脚本 - 聊天室与首页适配
130 lines
4.5 KiB
JavaScript
130 lines
4.5 KiB
JavaScript
/**
|
||
* 一次性资产脚本:从 github.com/rento666/douyin-emoji 下载表情图片存档,
|
||
* 拍平拷贝到 frontend/public/emoji/dy/ 并生成 manifest.json。
|
||
*
|
||
* 用法:node scripts/fetch-dy-emoji.mjs
|
||
* 产物(图片 + manifest.json)需提交进 git;重复执行会先清空目标目录。
|
||
*/
|
||
import { execFileSync } from "node:child_process";
|
||
import { createWriteStream, existsSync, mkdirSync, rmSync, statSync } from "node:fs";
|
||
import { readdir, writeFile, cp } from "node:fs/promises";
|
||
import os from "node:os";
|
||
import path from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
import { Readable } from "node:stream";
|
||
import { pipeline } from "node:stream/promises";
|
||
|
||
const REPO_ZIPS = [
|
||
// 国内网络下 GitHub 直连常被限速,优先走镜像加速(实测 gh-proxy.com 可达 MB/s 级)
|
||
"https://gh-proxy.com/https://github.com/rento666/douyin-emoji/archive/refs/heads/main.zip",
|
||
"https://codeload.github.com/rento666/douyin-emoji/zip/refs/heads/main",
|
||
"https://github.com/rento666/douyin-emoji/archive/refs/heads/main.zip",
|
||
];
|
||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||
const FRONTEND = path.resolve(HERE, "..");
|
||
const DEST = path.join(FRONTEND, "public", "emoji", "dy");
|
||
|
||
async function downloadZip(url, file) {
|
||
const res = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(120_000) });
|
||
if (!res.ok || !res.body) throw new Error(`下载失败 HTTP ${res.status}: ${url}`);
|
||
await pipeline(Readable.fromWeb(res.body), createWriteStream(file));
|
||
}
|
||
|
||
async function downloadZipWithFallback(file) {
|
||
let lastErr;
|
||
for (const url of REPO_ZIPS) {
|
||
try {
|
||
console.log(`尝试 ${url}`);
|
||
await downloadZip(url, file);
|
||
return;
|
||
} catch (e) {
|
||
lastErr = e;
|
||
console.warn(`下载失败:${e.message}`);
|
||
}
|
||
}
|
||
throw lastErr;
|
||
}
|
||
|
||
function extractZip(zip, dir) {
|
||
// Win10+/Linux 自带 bsdtar,可直接解 zip,无需额外依赖
|
||
execFileSync("tar", ["-xf", zip, "-C", dir], { stdio: "inherit" });
|
||
}
|
||
|
||
async function collectPng(dir) {
|
||
const out = [];
|
||
for (const name of await readdir(dir)) {
|
||
const p = path.join(dir, name);
|
||
if (statSync(p).isDirectory()) out.push(...(await collectPng(p)));
|
||
else if (name.toLowerCase().endsWith(".png")) out.push(p);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
async function main() {
|
||
if (!existsSync(DEST)) mkdirSync(DEST, { recursive: true });
|
||
const tmp = await readdir(os.tmpdir()).then(() => os.tmpdir());
|
||
const work = path.join(tmp, `dy-emoji-${Date.now()}`);
|
||
mkdirSync(work, { recursive: true });
|
||
const zip = path.join(work, "repo.zip");
|
||
|
||
console.log("下载仓库压缩包…");
|
||
await downloadZipWithFallback(zip);
|
||
console.log("解压…");
|
||
extractZip(zip, work);
|
||
const repo = path.join(work, "douyin-emoji-main", "dy_emojis");
|
||
|
||
const tosDir = path.join(repo, "tos_emoji");
|
||
const fusDir = path.join(repo, "fus_emoji");
|
||
if (!existsSync(tosDir) || !existsSync(fusDir)) {
|
||
throw new Error(`仓库结构不符合预期:${repo}`);
|
||
}
|
||
|
||
const tosFiles = await collectPng(tosDir);
|
||
const fusFiles = await collectPng(fusDir);
|
||
if (tosFiles.length === 0 || fusFiles.length === 0) {
|
||
throw new Error(`表情文件为空:tos=${tosFiles.length} fus=${fusFiles.length}`);
|
||
}
|
||
|
||
// 拍平拷贝:目标目录清空后写入,文件名唯一性校验
|
||
rmSync(DEST, { recursive: true, force: true });
|
||
mkdirSync(DEST, { recursive: true });
|
||
const seen = new Map();
|
||
const tos = [];
|
||
const fus = [];
|
||
let bytes = 0;
|
||
for (const [group, files] of [
|
||
["tos", tosFiles],
|
||
["fus", fusFiles],
|
||
]) {
|
||
for (const f of files) {
|
||
const key = path.basename(f, ".png");
|
||
if (!/^[A-Za-z0-9_-]+$/.test(key)) {
|
||
throw new Error(`文件名含非法字符(会破坏短代码):${key}`);
|
||
}
|
||
if (seen.has(key)) throw new Error(`文件名冲突:${key}(${f} 与 ${seen.get(key)})`);
|
||
seen.set(key, f);
|
||
await cp(f, path.join(DEST, `${key}.png`));
|
||
bytes += statSync(f).size;
|
||
(group === "tos" ? tos : fus).push(key);
|
||
}
|
||
}
|
||
tos.sort();
|
||
fus.sort();
|
||
|
||
const manifest = { tos, fus };
|
||
await writeFile(path.join(DEST, "manifest.json"), JSON.stringify(manifest), "utf8");
|
||
|
||
console.log(
|
||
`完成:tos=${tos.length} 张,fus=${fus.length} 张,共 ${(bytes / 1024 / 1024).toFixed(1)}MB → ${DEST}`
|
||
);
|
||
console.log(`manifest.json:${path.join(DEST, "manifest.json")}`);
|
||
|
||
// 清理临时目录
|
||
rmSync(work, { recursive: true, force: true });
|
||
}
|
||
|
||
main().catch((e) => {
|
||
console.error(e);
|
||
process.exit(1);
|
||
});
|