docs: document DEVPLACE_DISABLE_SERVICES env var, news router, modal class toggle, and news service behavior

This commit is contained in:
2026-05-12 10:45:52 +00:00
parent 331e19b14e
commit 388d4e3af8
36 changed files with 1232 additions and 301 deletions
+69 -5
View File
@@ -1,4 +1,5 @@
import logging
import os
import re
from datetime import datetime
@@ -6,7 +7,7 @@ import httpx
from devplacepy.database import get_table
from devplacepy.services.base import BaseService
from devplacepy.utils import generate_uid
from devplacepy.utils import generate_uid, make_combined_slug
logger = logging.getLogger(__name__)
@@ -24,6 +25,23 @@ def _get_setting(key: str, default: str) -> str:
return row.get("value", default)
def _get_ai_key() -> str:
key = os.environ.get("NEWS_AI_KEY")
if key:
return key
table = get_table("site_settings")
row = table.find_one(key="news_ai_key")
if row:
return row.get("value", "")
key = os.environ.get("OPENROUTER_API_KEY")
if key:
return key
key = os.environ.get("OPENAI_API_KEY")
if key:
return key
return ""
def _extract_grade(text: str) -> int | None:
match = re.search(r"\d+", text.strip())
if match:
@@ -79,17 +97,27 @@ class NewsService(BaseService):
news_table = get_table("news")
images_table = get_table("news_images")
sync_table = get_table("news_sync")
synced_ids = set()
for entry in sync_table.find():
synced_ids.add(entry["external_id"])
new_count = 0
updated_count = 0
draft_count = 0
failed_count = 0
skipped_count = 0
for article in articles:
external_id = article.get("guid", "")
if not external_id:
continue
if external_id in synced_ids:
skipped_count += 1
continue
grade = await self._grade_article(article, ai_url, ai_model)
now = datetime.utcnow().isoformat()
@@ -97,23 +125,28 @@ class NewsService(BaseService):
grade_val = 0
auto_published = False
failed_count += 1
sync_status = "grading_failed"
elif grade < threshold:
grade_val = grade
auto_published = False
draft_count += 1
sync_status = "graded"
else:
grade_val = grade
auto_published = True
sync_status = "graded"
is_published = "published" if auto_published else "draft"
existing = news_table.find_one(external_id=external_id)
if existing:
existing_slug = existing.get("slug", "")
news_table.update({
"id": existing["id"],
"grade": grade_val,
"status": is_published,
"title": article.get("title", ""),
"slug": existing_slug or make_combined_slug(article.get("title", "") or "news", existing["uid"]),
"description": (article.get("description", "") or "")[:5000],
"url": article.get("link", ""),
"source_name": article.get("feed_name", ""),
@@ -127,8 +160,10 @@ class NewsService(BaseService):
article_uid = existing["uid"]
else:
article_uid = generate_uid()
article_slug = make_combined_slug(article.get("title", "") or "news", article_uid)
news_table.insert({
"uid": article_uid,
"slug": article_slug,
"external_id": external_id,
"title": article.get("title", ""),
"description": (article.get("description", "") or "")[:5000],
@@ -144,6 +179,23 @@ class NewsService(BaseService):
})
new_count += 1
existing_sync = sync_table.find_one(external_id=external_id)
if existing_sync:
sync_table.update({
"id": existing_sync["id"],
"status": sync_status,
"synced_at": now,
}, ["id"])
else:
sync_table.insert({
"uid": generate_uid(),
"external_id": external_id,
"status": sync_status,
"synced_at": now,
})
synced_ids.add(external_id)
link = article.get("link", "")
if link:
fresh_images = await _get_article_images(link)
@@ -155,7 +207,8 @@ class NewsService(BaseService):
"alt_text": img.get("alt_text", ""),
})
self.log(f"New {new_count}, updated {updated_count}, draft {draft_count}, grading failed {failed_count}")
self.log(f"New {new_count}, updated {updated_count}, draft {draft_count}, "
f"grading failed {failed_count}, skipped {skipped_count}")
async def _grade_article(self, article: dict, ai_url: str, ai_model: str) -> int | None:
title = (article.get("title", "") or "")[:500]
@@ -180,14 +233,25 @@ class NewsService(BaseService):
headers = {
"Content-Type": "application/json",
}
ai_key = _get_ai_key()
if ai_key:
headers["Authorization"] = f"Bearer {ai_key}"
try:
async with httpx.AsyncClient(timeout=15.0) as client:
resp = await client.post(ai_url, json=payload, headers=headers)
resp.raise_for_status()
if resp.status_code != 200:
self.log(f"AI grading returned {resp.status_code}: {resp.text[:200]}")
resp.raise_for_status()
result = resp.json()
text = result.get("choices", [{}])[0].get("message", {}).get("content", "")
return _extract_grade(text)
if not text:
self.log(f"AI grading returned empty content for: {title[:60]}")
return None
grade = _extract_grade(text)
if grade is None:
self.log(f"AI grading returned unparseable response: {text[:100]}")
return grade
except Exception as e:
self.log(f"AI grading failed: {e}")
self.log(f"AI grading failed for '{title[:50]}': {e}")
return None