From c7a8e0538578e041b38898e06012d000021207d9 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Fri, 9 Oct 2026 14:19:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=B8=80=E8=A8=80):=20=E4=B8=80=E8=A8=80?= =?UTF-8?q?=E6=8C=A4=E5=9C=A8=E8=A7=92=E8=90=BD=E9=87=8C=EF=BC=8C=E5=87=BA?= =?UTF-8?q?=E5=A4=84=E8=A2=AB=E6=88=AA=E6=88=90=E3=80=8C=E6=9D=A5=E8=87=AA?= =?UTF-8?q?=20Structure=20and=20Interpretatio=E2=80=A6=E3=80=8D=E3=80=81?= =?UTF-8?q?=E4=B8=8D=E7=9F=A5=E9=81=93=E7=82=B9=E4=B8=80=E4=B8=8B=E8=83=BD?= =?UTF-8?q?=E6=8D=A2=E2=80=94=E2=80=94=E6=94=B9=E6=88=90=E5=BC=95=E5=8F=B7?= =?UTF-8?q?=20+=20=E5=8F=A5=E5=AD=90=20+=20=E3=80=8C=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E8=B0=81=E3=80=8A=E5=87=BA=E5=A4=84=E3=80=8B=C2=B7=20=E5=88=86?= =?UTF-8?q?=E7=B1=BB=E3=80=8D+=20=E6=8D=A2=E4=B8=80=E5=8F=A5=E6=8C=89?= =?UTF-8?q?=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 设计稿「一言重设计」A 版: - 句子一行,长句(数据集 p90 44 字、最长 255 字)截断,点句子弹出全文 - 出处写成「雷电将军《原神》· 游戏」:接口多下发 fromWho 和分类名,「佚名」「网络」这类充数的出处不写 - 「换一句」是个看得见的圆按钮,换的时候转一圈 - 契约 quoteSchema 从 string | Record 收成定形对象 - .env.example 说明本机怎么把 HITOKOTO_DIRECTORY 指到 sentences-bundle(不指就一直是内置的三句) Co-Authored-By: Claude Opus 5.5 --- .env.example | 3 + apps/api/src/routes/site.ts | 50 +++-- apps/web/src/oj/problem/list.vue | 5 +- apps/web/src/shared/components/Hitokoto.vue | 197 +++++++++++++++----- packages/contract/src/site.ts | 12 +- 5 files changed, 200 insertions(+), 67 deletions(-) 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