- DB: segments 用户输入文本(Alembic 0002) - Chat: 阶段检测/阶段提示/回复限制,编排与访谈/画像 prompts 调整 - Memoir: 忠实度检查 agent,叙事与分类等链路更新 - Core: agent 日志、Alembic 启动、LangChain/日志/配置等 - Story: time_hints;Memory 检索与相关测试 - Expo: 助手头像、会话页与消息拆分、实时会话与文案/i18n - Docs/scripts/tests: 迁移脚本、LLM JSON/记忆检索文档、新增单测
117 lines
3.2 KiB
Python
117 lines
3.2 KiB
Python
"""
|
||
从标题/正文推断 Story 的人生时间键,供章节内按发生顺序排序。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from datetime import datetime
|
||
from typing import Any
|
||
|
||
|
||
_YEAR_IN_TITLE = re.compile(r"(?:^|[\s·,,])(?P<y>(?:19|20)\d{2})(?:年|\s|·|$)")
|
||
_YEAR_ANYWHERE = re.compile(r"(?:19|20)\d{2}")
|
||
|
||
|
||
def parse_year_from_title(title: str | None) -> int | None:
|
||
"""从「1999年 · 标题」或标题中的四位年份提取年份。"""
|
||
if not title:
|
||
return None
|
||
m = _YEAR_IN_TITLE.search(title.strip())
|
||
if m:
|
||
try:
|
||
return int(m.group("y"))
|
||
except ValueError:
|
||
return None
|
||
return None
|
||
|
||
|
||
def parse_year_from_text(text: str | None, *, max_chars: int = 4000) -> int | None:
|
||
"""从正文前若干字中取第一个 plausible 四位年份。"""
|
||
if not text:
|
||
return None
|
||
chunk = (text or "")[:max_chars]
|
||
m = _YEAR_ANYWHERE.search(chunk)
|
||
if m:
|
||
try:
|
||
y = int(m.group(0))
|
||
if 1900 <= y <= 2100:
|
||
return y
|
||
except (ValueError, TypeError):
|
||
return None
|
||
return None
|
||
|
||
|
||
def parse_time_start_year(time_start: str | None) -> int | None:
|
||
"""time_start 存 'YYYY' 或 'YYYY-MM' 等,取年份用于排序。"""
|
||
if not time_start or not str(time_start).strip():
|
||
return None
|
||
s = str(time_start).strip()
|
||
m = re.match(r"^(\d{4})", s)
|
||
if m:
|
||
try:
|
||
y = int(m.group(1))
|
||
if 1900 <= y <= 2100:
|
||
return y
|
||
except ValueError:
|
||
return None
|
||
return None
|
||
|
||
|
||
def apply_infer_story_time_start_to_model(story: Any) -> None:
|
||
"""将推断的 YYYY 写入 `story.time_start`(已有合法值则保留)。"""
|
||
ts = infer_story_time_start(
|
||
title=getattr(story, "title", None),
|
||
canonical_markdown=getattr(story, "canonical_markdown", None),
|
||
existing_time_start=getattr(story, "time_start", None),
|
||
)
|
||
if ts:
|
||
story.time_start = ts
|
||
|
||
|
||
def infer_story_time_start(
|
||
*,
|
||
title: str | None,
|
||
canonical_markdown: str | None,
|
||
existing_time_start: str | None = None,
|
||
) -> str | None:
|
||
"""
|
||
返回统一 `YYYY` 字符串;若无法推断则返回 None。
|
||
已有 time_start 合法则保留。
|
||
"""
|
||
y_existing = parse_time_start_year(existing_time_start)
|
||
if y_existing is not None:
|
||
return str(y_existing)
|
||
|
||
y_title = parse_year_from_title(title)
|
||
if y_title is not None:
|
||
return str(y_title)
|
||
|
||
y_body = parse_year_from_text(canonical_markdown)
|
||
if y_body is not None:
|
||
return str(y_body)
|
||
|
||
return None
|
||
|
||
|
||
def life_sort_key_parts(
|
||
*,
|
||
time_start: str | None,
|
||
title: str | None,
|
||
created_at: datetime | None,
|
||
story_id: str,
|
||
) -> tuple[int, int, str]:
|
||
"""
|
||
用于 sorted(...): 人生发生顺序(早→晚)。
|
||
无年份时排在后面(9999),再以创建时间、id 稳定 tie-break。
|
||
"""
|
||
y = parse_time_start_year(time_start)
|
||
if y is None:
|
||
y = parse_year_from_title(title)
|
||
if y is None:
|
||
y = 9999
|
||
sub = 0
|
||
if created_at is not None:
|
||
sub = int(created_at.timestamp())
|
||
return (y, sub, story_id)
|