From f58b12bd349c65fdbe2093290dfd60f733a15b54 Mon Sep 17 00:00:00 2001 From: AeBoPi <109503402+AeBoPi@users.noreply.github.com> Date: Thu, 16 Jul 2026 01:27:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20MOYU=20=E5=86=85=E7=BD=AE?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E5=B9=B6=E4=BC=98=E5=8C=96=E8=AF=8D=E8=A1=A8?= =?UTF-8?q?=E5=92=8C=E9=BB=84=E5=8E=86=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/routes/modes.py | 3 ++- backend/core/json_content.py | 31 +++++++++++++++++++++- backend/core/mode_catalog.py | 7 ++++- backend/core/mode_registry.py | 12 +++++++++ backend/core/modes/builtin/almanac.json | 2 +- backend/core/modes/builtin/en/moyu.json | 28 ++++++++++++++++++++ backend/core/modes/builtin/moyu.json | 28 ++++++++++++++++++++ backend/core/vocab_store.py | 35 +++++++++++++++++++------ 8 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 backend/core/modes/builtin/en/moyu.json create mode 100644 backend/core/modes/builtin/moyu.json diff --git a/backend/api/routes/modes.py b/backend/api/routes/modes.py index 18b8ffaa..d706db8e 100644 --- a/backend/api/routes/modes.py +++ b/backend/api/routes/modes.py @@ -570,7 +570,8 @@ async def create_custom_mode(body: dict, request: Request, user_id: int = Depend body["mode_id"] = mode_id registry = get_registry() - if registry.is_builtin(mode_id): + existing_info = registry.get_mode_info(mode_id) + if existing_info and existing_info.source != "custom": return JSONResponse( {"error": f"Cannot override builtin mode: {mode_id}"}, status_code=409, diff --git a/backend/core/json_content.py b/backend/core/json_content.py index b0b3ed84..af3179be 100644 --- a/backend/core/json_content.py +++ b/backend/core/json_content.py @@ -161,6 +161,35 @@ def _normalize_almanac_chongsha(value: Any) -> str: return text.replace("(", "(").replace(")", ")") +def _rule_based_almanac_health_tip(result: dict[str, Any], fallback_tip: str) -> str: + yi = _normalize_almanac_list(result.get("fitness"), limit=3) + ji = _normalize_almanac_list(result.get("taboo"), limit=3) + solar_term = str(result.get("jieqi") or "").strip() + + def has_any(text: str, words: tuple[str, ...]) -> bool: + return any(word in text for word in words) + + if has_any(ji, ("动土", "修造", "破土", "安葬")): + return "少动土木,保持安稳" + if has_any(ji, ("出行", "远行", "赴任")): + return "行程从简,早些休息" + if has_any(ji, ("嫁娶", "订盟", "纳采")): + return "人情事务,宜缓不急" + if has_any(yi, ("沐浴", "扫舍", "除服")): + return "整理清洁,身心轻快" + if has_any(yi, ("祭祀", "祈福", "会友")): + return "静心处事,少些匆忙" + if solar_term in ("小暑", "大暑", "立夏", "夏至"): + return "暑热时节,清淡补水" + if solar_term in ("立冬", "小雪", "大雪", "冬至", "小寒", "大寒"): + return "寒时护暖,早睡养神" + if solar_term in ("立春", "雨水", "惊蛰", "春分", "清明", "谷雨"): + return "顺应春生,舒展身心" + if solar_term in ("立秋", "处暑", "白露", "秋分", "寒露", "霜降"): + return "秋燥渐起,润养作息" + return str(fallback_tip or "").strip() or "起居有常,饮食清淡" + + def _summarize_almanac_payload(result: dict[str, Any], fallback: dict[str, Any]) -> dict[str, Any]: solar_term = str(result.get("jieqi") or "").strip() or str(fallback.get("solar_term") or "").strip() yi = _normalize_almanac_list(result.get("fitness"), limit=3) or str(fallback.get("yi") or "").strip() @@ -300,7 +329,7 @@ async def _generate_almanac_health_tip(result: dict[str, Any], fallback_tip: str return cleaned[:20] except (LLMKeyMissingError, httpx.HTTPError, HTTPStatusError, OpenAIError, OSError, TypeError, ValueError): logger.warning("[ALMANAC] Failed to generate daily health tip", exc_info=True) - return fallback_tip + return _rule_based_almanac_health_tip(result, fallback_tip) def _make_aliyun_mt_client() -> AlimtClient | None: diff --git a/backend/core/mode_catalog.py b/backend/core/mode_catalog.py index 3c1b075d..7fe6679d 100644 --- a/backend/core/mode_catalog.py +++ b/backend/core/mode_catalog.py @@ -179,6 +179,12 @@ class CatalogItem: zh=CatalogText(name="课程表", tip="按周显示课程安排"), en=CatalogText(name="Timetable", tip="Weekly class schedule display"), ), + CatalogItem( + mode_id="MOYU", + category="more", + zh=CatalogText(name="摸鱼热榜", tip="全网时间线热榜"), + en=CatalogText(name="Moyu Hot List", tip="Timeline hot list across the web"), + ), CatalogItem( mode_id="MY_ADAPTIVE", category="custom", @@ -190,4 +196,3 @@ class CatalogItem: def builtin_catalog_map() -> dict[str, CatalogItem]: return {item.mode_id.upper(): item for item in BUILTIN_CATALOG} - diff --git a/backend/core/mode_registry.py b/backend/core/mode_registry.py index 6a28b1c2..8c7afb36 100644 --- a/backend/core/mode_registry.py +++ b/backend/core/mode_registry.py @@ -132,6 +132,12 @@ def load_json_mode(self, path: str, *, source: str = "custom") -> Optional[str]: f"[Registry] JSON mode {mode_id} shadows builtin — skipped" ) return None + existing = self._json_modes.get(mode_id) + if source == "custom" and existing and existing.info.source != "custom": + logger.warning( + f"[Registry] Custom JSON mode {mode_id} shadows {existing.info.source} — skipped" + ) + return None info = ModeInfo( mode_id=mode_id, @@ -243,6 +249,12 @@ def load_custom_mode_from_dict(self, mode_id: str, definition: dict, *, source: f"[Registry] Custom mode {mode_id} shadows builtin — skipped" ) return None + existing = self._json_modes.get(mode_id) + if source == "custom" and existing and existing.info.source != "custom": + logger.warning( + f"[Registry] Custom mode {mode_id} shadows {existing.info.source} — skipped" + ) + return None info = ModeInfo( mode_id=mode_id, diff --git a/backend/core/modes/builtin/almanac.json b/backend/core/modes/builtin/almanac.json index 9848ada0..14107d9c 100644 --- a/backend/core/modes/builtin/almanac.json +++ b/backend/core/modes/builtin/almanac.json @@ -32,7 +32,7 @@ "xingsu": "南方星日马", "suisha": "岁煞北", "chongsha": "狗日冲龙", - "health_tip": "春捂秋冻,适当增衣" + "health_tip": "起居有常,饮食清淡" } } ], diff --git a/backend/core/modes/builtin/en/moyu.json b/backend/core/modes/builtin/en/moyu.json new file mode 100644 index 00000000..86bb3b6d --- /dev/null +++ b/backend/core/modes/builtin/en/moyu.json @@ -0,0 +1,28 @@ +{ + "mode_id": "MOYU", + "display_name": "Moyu Hot List", + "icon": "global", + "cacheable": false, + "description": "Timeline hot list across the web", + "content": { + "type": "static", + "static_data": { + "image_url": "https://moyu.110x.de/eink.png?w=400&h=234" + } + }, + "layout": { + "status_bar": { + "style": "minimal" + }, + "body": [ + { + "type": "image", + "field": "image_url", + "width": 400, + "height": 234, + "x": 0, + "fit": "fill" + } + ] + } +} diff --git a/backend/core/modes/builtin/moyu.json b/backend/core/modes/builtin/moyu.json new file mode 100644 index 00000000..0c439d3e --- /dev/null +++ b/backend/core/modes/builtin/moyu.json @@ -0,0 +1,28 @@ +{ + "mode_id": "MOYU", + "display_name": "摸鱼热榜", + "icon": "global", + "cacheable": false, + "description": "全网时间线热榜", + "content": { + "type": "static", + "static_data": { + "image_url": "https://moyu.110x.de/eink.png?w=400&h=234" + } + }, + "layout": { + "status_bar": { + "style": "minimal" + }, + "body": [ + { + "type": "image", + "field": "image_url", + "width": 400, + "height": 234, + "x": 0, + "fit": "fill" + } + ] + } +} diff --git a/backend/core/vocab_store.py b/backend/core/vocab_store.py index ace104cb..c84aff95 100644 --- a/backend/core/vocab_store.py +++ b/backend/core/vocab_store.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import os from datetime import datetime, timedelta from pathlib import Path from typing import Any @@ -21,19 +22,32 @@ _DATA_DIR = Path(__file__).resolve().parent / "vocab_data" +def _auto_seed_decks() -> set[str] | None: + raw = os.getenv("VOCAB_AUTO_SEED_DECKS", "all").strip() + if raw.lower() == "all": + return None + decks = {part.strip() for part in raw.split(",") if part.strip()} + return decks or {DEFAULT_DECK_ID} + + async def seed_builtin_vocab() -> None: if not _DATA_DIR.exists(): return now = datetime.now().isoformat() db = await get_main_db() + allowed_decks = _auto_seed_decks() for path in sorted(_DATA_DIR.glob("*.json")): + deck_id = path.stem + if allowed_decks is not None and deck_id not in allowed_decks: + continue try: items = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): continue if not isinstance(items, list): continue + rows: list[tuple[str, str, str, str, str, int, str]] = [] for item in items: if not isinstance(item, dict): continue @@ -41,22 +55,27 @@ async def seed_builtin_vocab() -> None: definition = str(item.get("definition") or "").strip() if not word or not definition: continue - await db.execute( - """ - INSERT OR IGNORE INTO vocab_items - (deck_id, word, phonetic, definition, example, difficulty, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?) - """, + rows.append( ( - str(item.get("deck_id") or DEFAULT_DECK_ID), + str(item.get("deck_id") or deck_id or DEFAULT_DECK_ID), word, str(item.get("phonetic") or ""), definition, str(item.get("example") or ""), int(item.get("difficulty") or 1), now, - ), + ) ) + if not rows: + continue + await db.executemany( + """ + INSERT OR IGNORE INTO vocab_items + (deck_id, word, phonetic, definition, example, difficulty, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + rows, + ) await db.commit()