diff --git a/.env.example b/.env.example index d795880..1f42ecc 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,6 @@ TRIAL_CONCURRENCY=2 ALLOWED_WS_ORIGINS= OJ2_DEV_USERNAME=student OJ2_DEV_PASSWORD=student123 +# 一言数据集(hitokoto 官方的 sentences-bundle)。默认 data/hitokoto;本机放在仓库旁边的话指过去, +# 读不到就回落到内置的三句,不影响启动 +# HITOKOTO_DIRECTORY=../sentences-bundle diff --git a/apps/api/src/routes/site.ts b/apps/api/src/routes/site.ts index 403f058..7538455 100644 --- a/apps/api/src/routes/site.ts +++ b/apps/api/src/routes/site.ts @@ -39,29 +39,40 @@ const fallbackQuotes = [ { hitokoto: "程序首先是写给人读的,其次才是让机器执行。", from: "Structure and Interpretation of Computer Programs", + fromWho: null, + type: null, }, - { hitokoto: "把大问题拆成足够小的问题,答案就会浮现。", from: "判题狗" }, - { hitokoto: "一次没通过,只是多得到了一条线索。", from: "判题狗" }, -] + { + hitokoto: "把大问题拆成足够小的问题,答案就会浮现。", + from: "判题狗", + fromWho: null, + type: null, + }, + { hitokoto: "一次没通过,只是多得到了一条线索。", from: "判题狗", fromWho: null, type: null }, +] satisfies Quote[] // categories.json 里的 path 形如 "./sentences/a.json",12 个分类合计 20MB。 -// 按分类懒加载并常驻缓存,但**只留前端用得上的两个字段** —— 原样缓存的话, -// 解析后的对象要占 60~100MB,而 api 容器只有 512m。裁完全量也就几 MB。 -let categoryPaths: string[] | null = null +// 按分类懒加载并常驻缓存,但**只留前端用得上的几个字段**(句子、出处、谁说的、分类名)—— +// 原样缓存的话,解析后的对象要占 60~100MB,而 api 容器只有 512m。裁完全量也就几 MB。 +let categories: { path: string; name: string | null }[] | null = null const sentenceCache = new Map() -async function loadSentences(path: string) { +async function loadSentences(path: string, type: string | null) { const cached = sentenceCache.get(path) if (cached) return cached const raw = (await Bun.file(resolve(config.hitokotoDirectory, path)).json()) as { hitokoto?: unknown from?: unknown + from_who?: unknown }[] + const text = (value: unknown) => (typeof value === "string" && value.trim() ? value.trim() : null) const rows = (Array.isArray(raw) ? raw : []) .filter((it) => typeof it.hitokoto === "string" && it.hitokoto.length > 0) .map((it) => ({ hitokoto: it.hitokoto as string, - from: typeof it.from === "string" ? it.from : "佚名", + from: text(it.from) ?? "佚名", + fromWho: text(it.from_who), + type, })) if (rows.length === 0) throw new Error(`empty hitokoto category: ${path}`) sentenceCache.set(path, rows) @@ -69,18 +80,19 @@ async function loadSentences(path: string) { } async function randomQuote() { - if (!categoryPaths) { - const categories = (await Bun.file( - resolve(config.hitokotoDirectory, "categories.json"), - ).json()) as { path?: string }[] - const paths = categories - .map((it) => it.path) - .filter((it): it is string => typeof it === "string") - if (paths.length === 0) throw new Error("no hitokoto categories") - categoryPaths = paths + if (!categories) { + const list = (await Bun.file(resolve(config.hitokotoDirectory, "categories.json")).json()) as { + path?: unknown + name?: unknown + }[] + const parsed = list + .filter((it): it is { path: string; name?: unknown } => typeof it.path === "string") + .map((it) => ({ path: it.path, name: typeof it.name === "string" ? it.name : null })) + if (parsed.length === 0) throw new Error("no hitokoto categories") + categories = parsed } - const path = categoryPaths[Math.floor(Math.random() * categoryPaths.length)]! - const sentences = await loadSentences(path) + const category = categories[Math.floor(Math.random() * categories.length)]! + const sentences = await loadSentences(category.path, category.name) return sentences[Math.floor(Math.random() * sentences.length)]! } diff --git a/apps/web/src/oj/problem/list.vue b/apps/web/src/oj/problem/list.vue index 6eb8c47..8dfe041 100644 --- a/apps/web/src/oj/problem/list.vue +++ b/apps/web/src/oj/problem/list.vue @@ -746,9 +746,8 @@ const gridColumns = computed(() => } .hitokoto { - flex: 0 1 420px; - max-width: 420px; - font-size: 13px; + flex: 0 1 460px; + max-width: 460px; } .anon { diff --git a/apps/web/src/shared/components/Hitokoto.vue b/apps/web/src/shared/components/Hitokoto.vue index 675be9c..f094c57 100644 --- a/apps/web/src/shared/components/Hitokoto.vue +++ b/apps/web/src/shared/components/Hitokoto.vue @@ -1,71 +1,180 @@ + + diff --git a/packages/contract/src/site.ts b/packages/contract/src/site.ts index 84c0381..9c812ee 100644 --- a/packages/contract/src/site.ts +++ b/packages/contract/src/site.ts @@ -16,7 +16,17 @@ export const onlineCountSchema = z.object({ count: z.number().int().nonnegative(), }) -export const quoteSchema = z.union([z.string(), z.record(z.string(), z.unknown())]) +/** + * 一言。`from` 是出处(作品),`fromWho` 是谁说的,`type` 是数据集的分类名(动画、诗词……), + * 后两个数据集里可能没有。原来是 string | Record 的宽类型,前端拿到再猜形状; + * 出参是后端自己拼的,直接给定形 + */ +export const quoteSchema = z.object({ + hitokoto: z.string(), + from: z.string(), + fromWho: z.string().nullable(), + type: z.string().nullable(), +}) export type WebsiteConfig = z.infer export type Quote = z.infer