feat: 更新首页信息处理模型,支持内存缓存与过期机制;添加全量目录同步脚本
This commit is contained in:
@@ -4,6 +4,7 @@ Only allowlisted platform paths are fetched. HTML is converted to inert text;
|
||||
platform content is never an instruction and never supplies model/tool endpoints.
|
||||
"""
|
||||
import asyncio
|
||||
import calendar
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
@@ -131,10 +132,10 @@ def next_offset(data, path, current):
|
||||
raise WqError("平台分页信息无法识别", "invalid_response") from None
|
||||
|
||||
|
||||
async def all_pages(client, path):
|
||||
async def all_pages(client, path, limit=None):
|
||||
output, offset = [], 0
|
||||
for _ in range(100):
|
||||
data = await client.get(path, params={"offset": offset})
|
||||
data = await client.get(path, params={"offset": offset, **({"limit": limit} if limit else {})})
|
||||
output.extend(rows(data))
|
||||
offset = next_offset(data, path, offset)
|
||||
if offset is None:
|
||||
@@ -160,14 +161,17 @@ def competition(item, user):
|
||||
async def source(client, module, user, offset, cid):
|
||||
if module == "messages":
|
||||
path = "/users/self/messages"
|
||||
data = await client.get(path, params={"offset": offset})
|
||||
items = []
|
||||
for item in rows(data):
|
||||
for item in await all_pages(client, path, limit=100):
|
||||
created = instant(item.get("dateCreated"))
|
||||
if not created or not month_start() <= created <= now():
|
||||
continue
|
||||
body, links = plain(item.get("description"))
|
||||
items.append(dict(id=text(item.get("id")), title=text(item.get("title")) or "平台消息",
|
||||
type=text(item.get("type")), date=text(item.get("dateCreated")),
|
||||
description=body, links=links, url="https://platform.worldquantbrain.com/messages/" + ("announcements" if item.get("type") == "ANNOUNCEMENT" else "notifications")))
|
||||
return dict(items=items, offset=offset, next_offset=next_offset(data, path, offset), total=number(data.get("count")))
|
||||
items.sort(key=lambda item: instant(item["date"]), reverse=True)
|
||||
return dict(items=items)
|
||||
if module == "leaderboard":
|
||||
data = await client.get("/consultant/boards/leader", params={"user": user})
|
||||
matches = [r for r in rows(data) if r.get("user") == user]
|
||||
@@ -227,8 +231,74 @@ def config_version(config):
|
||||
return digest([config.description_model, config.base_url, config.api_key_encrypted, config.protocol])
|
||||
|
||||
|
||||
def month_start():
|
||||
"""One calendar month before the Eastern wall time, clamped at month end."""
|
||||
at = now().astimezone(EASTERN)
|
||||
year, month = (at.year, at.month - 1) if at.month > 1 else (at.year - 1, 12)
|
||||
return at.replace(year=year, month=month, day=min(at.day, calendar.monthrange(year, month)[1]))
|
||||
|
||||
|
||||
def message_page(request, user, offset):
|
||||
"""Return a detached page; neither source text nor AI output enters an ORM session."""
|
||||
cached = request.app.state.home_message_cache.get(user)
|
||||
if cached is None:
|
||||
return None
|
||||
if now() - cached.created_at >= timedelta(minutes=15):
|
||||
del request.app.state.home_message_cache[user]
|
||||
return None
|
||||
content = None
|
||||
if cached.content is not None:
|
||||
# Enforce the rolling boundary even on cached reads and generation commits.
|
||||
filtered = [item for item in cached.content["items"]
|
||||
if month_start() <= instant(item["date"]) <= now()]
|
||||
if len(filtered) != len(cached.content["items"]):
|
||||
cached.analyses.clear()
|
||||
cached.content = {"items": filtered}
|
||||
items = cached.content["items"]
|
||||
content = dict(items=items[offset:offset + 10], total=len(items), offset=offset,
|
||||
next_offset=offset + 10 if offset + 10 < len(items) else None)
|
||||
return SimpleNamespace(content=content, version=digest(content), fetched_at=cached.fetched_at,
|
||||
error=cached.error, analysis=cached.analyses.get(offset))
|
||||
|
||||
|
||||
async def read_messages(request, user, offset, refresh):
|
||||
"""Keep only a 15-minute in-process cache; restart/expiry discards interpretations too."""
|
||||
async with lock(request, (user, "messages")):
|
||||
cache = request.app.state.home_message_cache
|
||||
for owner, entry in list(cache.items()):
|
||||
if now() - entry.created_at >= timedelta(minutes=15):
|
||||
del cache[owner]
|
||||
cached = cache.get(user)
|
||||
if cached is not None and not refresh:
|
||||
return message_page(request, user, offset)
|
||||
try:
|
||||
content = await source(request.app.state.runner.client, "messages", user, 0, "")
|
||||
error = None
|
||||
except WqError as exc:
|
||||
content, error = None, str(exc)
|
||||
if await identity(request) != user:
|
||||
raise HTTPException(409, "账户已变化,请重新读取信息")
|
||||
if cached is None:
|
||||
cached = SimpleNamespace(content=None, fetched_at=None, created_at=now(), error=None, analyses={})
|
||||
cache[user] = cached
|
||||
cached.error = error
|
||||
if content is not None:
|
||||
if cached.content != content:
|
||||
cached.analyses.clear()
|
||||
cached.content, cached.fetched_at = content, now()
|
||||
return message_page(request, user, offset)
|
||||
|
||||
|
||||
async def cached_record(request, db, user, module, offset, key):
|
||||
if module == "messages":
|
||||
return message_page(request, user, offset)
|
||||
return await db.get(HomeInformation, (user, key))
|
||||
|
||||
|
||||
async def read_record(request, user, module, offset, cid, refresh=False):
|
||||
key = resource_key(module, offset, cid)
|
||||
if module == "messages":
|
||||
return await read_messages(request, user, offset, refresh)
|
||||
async with lock(request, (user, key)):
|
||||
async with request.app.state.sessions() as db:
|
||||
record = await db.get(HomeInformation, (user, key))
|
||||
@@ -303,12 +373,16 @@ async def output(request, user, module, record):
|
||||
config = await db.get(AISettings, 1)
|
||||
_, version = await analysis_sources(request, user, module, record)
|
||||
analysis = record.analysis
|
||||
if module == "messages" and analysis and analysis["source_version"] != version:
|
||||
request.app.state.home_message_cache[user].analyses.pop(record.content["offset"], None)
|
||||
analysis = None
|
||||
if analysis:
|
||||
analysis = {**analysis, "outdated": analysis["source_version"] != version or analysis["config_version"] != config_version(config)}
|
||||
analysis.pop("config_version", None)
|
||||
return dict(content=temporal(record.content, module), fetched_at=record.fetched_at.replace(tzinfo=timezone.utc) if record.fetched_at else None, error=record.error,
|
||||
stale=bool(record.error) or bool(module == "leaderboard" and record.content and record.content["scope"] != "dailyOsmosisRank:" + now().astimezone(EASTERN).date().isoformat()) or bool(record.fetched_at and now() - record.fetched_at.replace(tzinfo=timezone.utc) > timedelta(hours=24)),
|
||||
source_version=record.version, analysis=analysis,
|
||||
ephemeral=module == "messages",
|
||||
can_generate=bool(config.description_model and config.api_key_encrypted and config.base_url and record.content))
|
||||
|
||||
|
||||
@@ -364,7 +438,7 @@ async def generate(request: Request, module: Module, offset: int = Query(0, ge=0
|
||||
raise HTTPException(409, "该模块正在生成解读,请稍候")
|
||||
async with generation_lock:
|
||||
async with request.app.state.sessions() as db:
|
||||
record = await db.get(HomeInformation, (user, key))
|
||||
record = await cached_record(request, db, user, module, offset, key)
|
||||
config = await db.get(AISettings, 1)
|
||||
if not config.description_model or not config.api_key_encrypted or not config.base_url:
|
||||
raise HTTPException(409, "请先配置基础信息处理模型及共享连接配置")
|
||||
@@ -396,13 +470,17 @@ async def generate(request: Request, module: Module, offset: int = Query(0, ge=0
|
||||
await ai.authorize(token_hash(request.cookies["wq_session"]))
|
||||
if await identity(request) != user:
|
||||
raise HTTPException(409, "账户已变化,请重新生成")
|
||||
async with lock(request, (user, key)):
|
||||
async with lock(request, (user, "messages" if module == "messages" else key)):
|
||||
async with request.app.state.sessions.begin() as db:
|
||||
record = await db.get(HomeInformation, (user, key))
|
||||
record = await cached_record(request, db, user, module, offset, key)
|
||||
current_config = await db.get(AISettings, 1)
|
||||
if record is None:
|
||||
raise HTTPException(409, "公告内存缓存已失效,请刷新后重新生成")
|
||||
_, current_version = await analysis_sources(request, user, module, record)
|
||||
if current_version != version or config_version(current_config) != fingerprint:
|
||||
raise HTTPException(409, "生成期间来源或模型配置已变化,已保留上次解读,请重新生成")
|
||||
record.analysis = dict(**interpretation, source_version=version, config_version=fingerprint,
|
||||
model=connection.model, generated_at=now().isoformat())
|
||||
if module == "messages":
|
||||
request.app.state.home_message_cache[user].analyses[offset] = record.analysis
|
||||
return await output(request, user, module, record)
|
||||
|
||||
Reference in New Issue
Block a user