fix: correct spelling of "Update" in commit message to "Update"

This commit is contained in:
2026-06-13 12:56:35 +00:00
parent aa1598009e
commit 6756c980cf
16 changed files with 473 additions and 88 deletions
+169 -58
View File
@@ -19,6 +19,9 @@ from devplacepy.services.bot.config import (
GIST_LANGUAGES,
HANDLE_SUFFIX_WORDS,
MAX_SIBLING_COMMENTS,
MAX_THREAD_REPLIES,
MENTION_REPLIES_PER_SESSION,
NOTIFICATION_CHECK_PROB,
PERSONAS,
PROJECT_STATUSES,
PROJECT_TYPES,
@@ -85,6 +88,7 @@ class DevPlaceBot:
)
self._session_comments = 0
self._session_comment_cap = 0
self._session_mention_replies = 0
self._post_cache: list[tuple[str, str, str, str, str]] = []
self._project_cache: list[tuple[str, str]] = []
self._bug_cache: list[tuple[str, str]] = []
@@ -412,6 +416,22 @@ class DevPlaceBot:
logger.debug("page summary count failed: %s", e)
return summary
async def _unread_notification_count(self) -> int:
try:
badge = self.b.page.locator(
"a[data-counter='notifications'] .nav-badge[data-counter-badge]"
).first
if await badge.count() == 0:
return 0
if await badge.get_attribute("hidden") is not None:
return 0
text = (await badge.inner_text() or "").strip()
digits = re.sub(r"[^0-9]", "", text)
return int(digits) if digits else 0
except Exception as e:
logger.debug("_unread_notification_count failed: %s", e)
return 0
async def _page_state(
self,
curl: str,
@@ -455,6 +475,7 @@ class DevPlaceBot:
"can_project": can_project,
"can_post": can_post,
"can_message": can_message,
"unread_notifications": await self._unread_notification_count(),
}
def _build_menu(self, page_state: dict) -> list[dict]:
@@ -519,6 +540,11 @@ class DevPlaceBot:
add("open_profile", "open another developer's profile")
add("search", "search the platform for a topic you care about")
add("browse", "browse a category or section")
if page != "notifications" and page_state.get("unread_notifications"):
add(
"check_notifications",
"open your notifications and reply to anyone who mentioned or replied to you",
)
add("navigate", "move to another section: feed, projects, gists, or news")
return menu
@@ -1888,69 +1914,108 @@ class DevPlaceBot:
logger.debug("_follow_user failed: %s", e)
return False
def _classify_notification(self, text: str) -> bool:
tl = text.lower()
return (
f"@{self.state.username.lower()}" in tl
or "mentioned you" in tl
or "replied to your" in tl
or "replied to you" in tl
or "tagged you" in tl
)
async def _collect_notifications(self) -> list[dict]:
b = self.b
item_selectors = [
".notification-item",
".notif-item",
"li.notification",
".notifications-list > li",
".notifications-list > a",
".notification",
"[data-notification-id]",
]
cards = b.page.locator(".notification-card")
try:
count = await cards.count()
except Exception:
count = 0
if count == 0:
return await self._collect_notifications_fallback()
items: list[dict] = []
seen_keys: set = set()
for sel in item_selectors:
loc = b.page.locator(sel)
for i in range(min(count, 30)):
card = cards.nth(i)
text = ""
try:
count = await loc.count()
body = card.locator(".notification-text").first
if await body.count() > 0:
text = (await body.inner_text() or "")[:600]
except Exception:
count = 0
if count == 0:
continue
for i in range(min(count, 30)):
el = loc.nth(i)
text = ""
if not text:
try:
text = (await el.inner_text() or "")[:600]
text = (await card.inner_text() or "")[:600]
except Exception:
text = ""
href = ""
try:
link = card.locator("a.card-link").first
if await link.count() > 0:
href = (await link.get_attribute("href")) or ""
except Exception:
href = ""
try:
href_attr = await el.get_attribute("href")
except Exception:
href_attr = None
if href_attr:
href = href_attr
else:
try:
inner = el.locator(
"a[href*='/posts/'], a[href*='/profile/']"
).first
if await inner.count() > 0:
href = (await inner.get_attribute("href")) or ""
except Exception:
href = ""
if not text and not href:
continue
key = hashlib.sha256(f"{href}|{text[:200]}".encode()).hexdigest()[:24]
if key in seen_keys:
continue
seen_keys.add(key)
tl = text.lower()
uname = self.state.username.lower()
is_mention = (
f"@{uname}" in tl
or "mentioned you" in tl
or "replied to your" in tl
or "replied to you" in tl
or "tagged you" in tl
)
items.append(
{"key": key, "href": href, "text": text, "is_mention": is_mention}
)
if items:
break
unread = False
try:
cls = (await card.get_attribute("class")) or ""
unread = "unread" in cls.split()
except Exception:
unread = False
if not text and not href:
continue
key = hashlib.sha256(f"{href}|{text[:200]}".encode()).hexdigest()[:24]
if key in seen_keys:
continue
seen_keys.add(key)
items.append(
{
"key": key,
"href": href,
"text": text,
"is_mention": self._classify_notification(text),
"unread": unread,
}
)
return items
async def _collect_notifications_fallback(self) -> list[dict]:
b = self.b
loc = b.page.locator(".notification-item, .notif-item, [data-notification-id]")
try:
count = await loc.count()
except Exception:
count = 0
items: list[dict] = []
seen_keys: set = set()
for i in range(min(count, 30)):
el = loc.nth(i)
try:
text = (await el.inner_text() or "")[:600]
except Exception:
text = ""
href = ""
try:
inner = el.locator("a[href]").first
if await inner.count() > 0:
href = (await inner.get_attribute("href")) or ""
except Exception:
href = ""
if not text and not href:
continue
key = hashlib.sha256(f"{href}|{text[:200]}".encode()).hexdigest()[:24]
if key in seen_keys:
continue
seen_keys.add(key)
items.append(
{
"key": key,
"href": href,
"text": text,
"is_mention": self._classify_notification(text),
"unread": True,
}
)
return items
async def _check_notifications(self) -> bool:
@@ -1964,17 +2029,25 @@ class DevPlaceBot:
unseen_mentions = [
n
for n in notifications
if n["is_mention"] and n["key"] not in self.state.notifications_seen
if n["is_mention"]
and n["unread"]
and n["key"] not in self.state.notifications_seen
]
self._log(
f"Notifications: {len(notifications)} ({len(unseen_mentions)} unseen mentions)"
)
self.state.mentions_received += len(unseen_mentions)
replied = 0
for n in unseen_mentions[:3]:
for n in unseen_mentions:
if self._session_mention_replies >= MENTION_REPLIES_PER_SESSION:
self._log(
f"Mention reply cap reached ({MENTION_REPLIES_PER_SESSION}/session), leaving the rest"
)
break
ok = await self._respond_to_mention(n["href"], n["text"])
if ok:
self.state.mentions_replied += 1
self._session_mention_replies += 1
replied += 1
self.state.notifications_seen.append(n["key"])
if len(self.state.notifications_seen) > 500:
@@ -2016,7 +2089,7 @@ class DevPlaceBot:
target = href
if target and not target.startswith("http"):
target = self.base_url + (href if href.startswith("/") else "/" + href)
if "/posts/" not in (target or ""):
if not target:
return False
try:
await b.goto(target)
@@ -2024,6 +2097,19 @@ class DevPlaceBot:
logger.debug("mention goto failed: %s", e)
return False
await b._idle(2.0, 4.0)
curl = await b.url()
if not any(k in curl for k in ("/posts/", "/gists/", "/projects/", "/news/")):
self._log("Mention target has no comment thread, skipping")
return False
thread_id = self._thread_id(curl)
if (
thread_id
and self.state.thread_reply_counts.get(thread_id, 0) >= MAX_THREAD_REPLIES
):
self._log(
f"Conversation taper on {thread_id[:12]} ({MAX_THREAD_REPLIES} replies), letting it rest"
)
return False
needle = f"@{self.state.username}"
comment_loc = None
comment_selectors = [
@@ -2073,7 +2159,14 @@ class DevPlaceBot:
logger.debug("reply-btn click failed: %s", e)
try:
post_body = (
await b.text(".post-content") or await b.text("article") or snippet
await b.text(".post-content")
or await b.text(".gist-detail-desc")
or await b.text(".news-detail-content")
or await b.text(".news-detail-desc")
or await b.text(".project-detail-desc")
or await b.text(".rendered-content")
or await b.text("article")
or snippet
)
except Exception:
post_body = snippet
@@ -2119,6 +2212,13 @@ class DevPlaceBot:
if await b.click(sel):
self.state.comments_posted += 1
self.state.replies_to_own_posts += 1
if thread_id:
self.state.thread_reply_counts[thread_id] = (
self.state.thread_reply_counts.get(thread_id, 0) + 1
)
if len(self.state.thread_reply_counts) > 500:
recent = list(self.state.thread_reply_counts.items())[-250:]
self.state.thread_reply_counts = dict(recent)
self._action("MENTION", f"replied to @{mentioner or '?'}: {reply[:60]}")
await asyncio.sleep(2)
return True
@@ -2660,6 +2760,7 @@ class DevPlaceBot:
self._session_comment_cap = random.randint(
SESSION_COMMENT_CAP_MIN, SESSION_COMMENT_CAP_MAX
)
self._session_mention_replies = 0
use_ai = False
try:
@@ -2691,14 +2792,24 @@ class DevPlaceBot:
await b.goto(f"{self.base_url}/feed")
await b._idle(0.8, 2.0)
unread = await self._unread_notification_count()
if unread:
self._log(f"Unread notifications badge: {unread}")
check_prob = NOTIFICATION_CHECK_PROB.get(mood, 1.0)
want_notifications = mood in ("normal", "deep") or (
unread > 0 and random.random() < check_prob
)
if mood in ("normal", "deep"):
await self._check_messages()
await b._idle(0.5, 1.5)
if want_notifications:
await self._check_notifications()
await b._idle(1.0, 3.0)
if mood in ("normal", "deep"):
await self._engage_community()
await b._idle(0.8, 2.0)
await b.goto(f"{self.base_url}/feed")
await b.goto(f"{self.base_url}/feed")
session_actions = 0
max_actions = session_len + random.randint(0, 5)
+9
View File
@@ -32,6 +32,15 @@ SESSION_COMMENT_CAP_MAX = 3
COMMENT_COOLDOWN_MIN_SECONDS = 60
COMMENT_COOLDOWN_MAX_SECONDS = 240
MENTION_REPLIES_PER_SESSION = 3
MAX_THREAD_REPLIES = 3
NOTIFICATION_CHECK_PROB = {
"lurking": 0.6,
"casual": 0.85,
"normal": 1.0,
"deep": 1.0,
}
HANDLE_SUFFIX_WORDS = [
"dev",
"codes",
+7 -1
View File
@@ -524,9 +524,15 @@ class LLMClient:
def _decision_prompt(page_state: dict, menu: list[dict], history: list[str]) -> str:
menu_lines = "\n".join(f"- {item['action']}: {item['desc']}" for item in menu)
recent = "; ".join(history[-8:]) if history else "nothing yet this visit"
unread = page_state.get("unread_notifications", 0)
unread_line = (
f"\nUNREAD NOTIFICATIONS: {unread} (someone may have mentioned or replied to you)"
if unread
else ""
)
return (
f"PAGE: {page_state.get('page', 'unknown')}\n"
f"VISIBLE: {page_state.get('visible', '')}\n"
f"VISIBLE: {page_state.get('visible', '')}{unread_line}\n"
f"MENU:\n{menu_lines}\n"
f"RECENT (this visit): {recent}"
)
+1
View File
@@ -36,6 +36,7 @@ class BotState:
voted_post_ids: list[str] = field(default_factory=list)
commented_post_ids: list[str] = field(default_factory=list)
notifications_seen: list[str] = field(default_factory=list)
thread_reply_counts: dict = field(default_factory=dict)
mentions_received: int = 0
mentions_replied: int = 0
replies_to_own_posts: int = 0