docs: add CLAUDE.md with project guidelines and conventions, document new devii admin quota reset endpoints and CLI commands, update README with coverage CI and devii_admin_daily_usd config, add European date normalization to ProjectForm model

This commit is contained in:
2026-06-08 22:30:25 +00:00
parent 841c1c7417
commit 547e4eda8c
45 changed files with 1844 additions and 99 deletions
+49 -26
View File
@@ -12,7 +12,7 @@ from faker import Faker
from devplacepy.services.bot.browser import BotBrowser
from devplacepy.services.bot.config import (
CATEGORIES, FEED_TOPICS, GIST_LANGUAGES, PERSONAS, PROJECT_STATUSES,
PROJECT_TYPES, SEARCH_TERMS,
PROJECT_TYPES, REACT_RATE_DEFAULT, REACT_RATES, SEARCH_TERMS,
)
from devplacepy.services.bot.llm import LLMClient
from devplacepy.services.bot.news_fetcher import NewsFetcher
@@ -777,56 +777,79 @@ class DevPlaceBot:
return True
return False
async def _reaction_content(self, bar: Any) -> str:
try:
container = bar.locator(
"xpath=ancestor::article[contains(concat(' ',normalize-space(@class),' '),' post-card ')][1]"
" | ancestor::div[contains(concat(' ',normalize-space(@class),' '),' comment ')][1]"
).first
if await container.count() > 0:
text = (await container.inner_text() or "").strip()
if text:
return text[:1200]
except Exception as e:
logger.debug("_reaction_content container failed: %s", e)
body = (await self.b.text(".post-content") or await self.b.text(".gist-detail-desc")
or await self.b.text(".news-detail-content") or await self.b.text(".news-detail-desc")
or await self.b.text(".project-detail-desc") or await self.b.text(".rendered-content")
or await self.b.text("article") or "")
return body[:1200]
async def _react_to_object(self) -> bool:
b = self.b
bars = b.page.locator(".reaction-bar[data-reaction-uid]")
count = await bars.count()
if count == 0:
return False
reacted = set(self.state.reacted_targets)
decided = set(self.state.reacted_targets)
react_rate = REACT_RATES.get(self.state.persona, REACT_RATE_DEFAULT)
for i in range(min(count, 20)):
bar = bars.nth(i)
uid = await bar.get_attribute("data-reaction-uid") or ""
rtype = await bar.get_attribute("data-reaction-type") or ""
if not uid:
if not uid or uid in decided:
continue
self.state.reacted_targets.append(uid)
if len(self.state.reacted_targets) > 2000:
self.state.reacted_targets = self.state.reacted_targets[-1000:]
if random.random() > react_rate:
self._log(f"Reaction: skipping {rtype} {uid[:12]} (persona reticence)")
return False
snippet = await self._reaction_content(bar)
if not snippet:
self._log(f"Reaction: no content for {rtype} {uid[:12]}")
return False
emoji = await self._generate(self.llm.select_reaction, snippet, self.state.persona)
if not emoji:
self._log(f"Reaction: nothing fitting for {rtype} {uid[:12]}")
return False
add_btn = bar.locator(".reaction-add-btn").first
if await add_btn.count() == 0:
continue
palette_btns = bar.locator(".reaction-palette-btn")
pcount = await palette_btns.count()
if pcount == 0:
continue
order = list(range(pcount))
random.shuffle(order)
chosen_idx = None
chosen_emoji = ""
for idx in order:
emoji = await palette_btns.nth(idx).get_attribute("data-reaction-emoji") or ""
if emoji and f"{uid}|{emoji}" not in reacted:
chosen_idx = idx
chosen_emoji = emoji
break
if chosen_idx is None:
continue
return False
palette_btn = bar.locator(
f".reaction-palette-btn[data-reaction-emoji='{emoji}']"
).first
if await palette_btn.count() == 0:
return False
try:
await add_btn.scroll_into_view_if_needed(timeout=2000)
await b._idle(0.2, 0.6)
await add_btn.click(timeout=3000)
await b._idle(0.3, 0.7)
await palette_btns.nth(chosen_idx).click(timeout=3000)
await palette_btn.click(timeout=3000)
except Exception as e:
logger.debug("_react_to_object click failed: %s", e)
continue
return False
try:
await bar.locator(".reaction-chip.reacted").first.wait_for(state="visible", timeout=4000)
except Exception as e:
logger.debug("_react_to_object confirm failed: %s", e)
self.state.reacted_targets.append(f"{uid}|{chosen_emoji}")
if len(self.state.reacted_targets) > 2000:
self.state.reacted_targets = self.state.reacted_targets[-1000:]
self.state.reactions_cast += 1
self._action("REACT", f"{chosen_emoji} on {rtype} {uid[:12]} (total {self.state.reactions_cast})")
self._action("REACT", f"{emoji} on {rtype} {uid[:12]} (total {self.state.reactions_cast})")
await b._idle(1.0, 2.0)
return True
return False
+12
View File
@@ -28,6 +28,18 @@ PERSONAS = [
"mentor",
]
REACT_RATES = {
"enthusiastic_junior": 0.50,
"hobbyist_maker": 0.40,
"storyteller": 0.30,
"mentor": 0.30,
"rebel": 0.25,
"academic_type": 0.20,
"minimalist": 0.12,
"grumpy_senior": 0.12,
}
REACT_RATE_DEFAULT = 0.20
CATEGORIES = ["devlog", "showcase", "question", "rant", "fun", "random"]
GIST_LANGUAGES = [
+29
View File
@@ -130,6 +130,35 @@ class LLMClient:
return c
return "random"
def select_reaction(self, content_snippet: str, persona: str = "") -> str:
from devplacepy.constants import REACTION_EMOJI
flavor = {
"enthusiastic_junior": "You react readily and warmly.",
"grumpy_senior": "You react rarely, only to genuinely notable content.",
"hobbyist_maker": "You react to anything hands-on, clever, or fun.",
"academic_type": "You react only to substantive, rigorous content.",
"minimalist": "You react very sparingly.",
"storyteller": "You react to anything with a human angle.",
"rebel": "You react to bold or contrarian takes.",
"mentor": "You react supportively to effort and learning.",
}.get(persona, "")
options = " ".join(REACTION_EMOJI)
verdict = self._call(
"You are a developer browsing a community feed. Decide whether a post deserves an "
"emoji reaction and which single emoji best fits its content and sentiment. "
f"{flavor} Reply with EXACTLY one of these emojis: {options} "
"or the word NONE if the content is mundane or would not move you to react. "
"Output only the emoji or NONE, nothing else.",
f"Post:\n{content_snippet[:1200]}",
temperature=0.4,
)
text = verdict or ""
for emoji in REACTION_EMOJI:
if emoji in text or emoji.replace("\ufe0f", "") in text:
return emoji
return ""
def generate_comment(self, post_snippet: str, persona: str = "",
mention_target: str = "", parent_context: str = "") -> str:
extras = {