feat: complete huihui square avatar workflows
This commit is contained in:
@@ -190,6 +190,32 @@ class AIService:
|
||||
请对上面的评论写一条友善自然的回复,{word_min}~{word_max}字,直接输出回复内容。"""
|
||||
return await self._call_api(db, prompt, system_prompt, max_tokens=150)
|
||||
|
||||
async def generate_thread_reply(
|
||||
self, db: AsyncSession, article_title: str, root_comment: str,
|
||||
reply_context: str, personality_prompt: str,
|
||||
word_min: int = 15, word_max: int = 60
|
||||
) -> tuple[str, int]:
|
||||
"""结合文章、原评论和上下文生成评论回复链中的下一句。"""
|
||||
system_prompt = f"""你是一名真实的社区用户。{personality_prompt}
|
||||
|
||||
重要规则:
|
||||
- 回复必须积极正面、文明友善,不含任何敏感违规内容
|
||||
- 要自然接住对方的话,不要机械复述
|
||||
- 不要透露自己是AI或虚拟用户"""
|
||||
prompt = f"""文章标题:{article_title}
|
||||
原评论:{root_comment}
|
||||
当前对话上下文:{reply_context}
|
||||
|
||||
请结合文章、原评论和当前对话,写一条自然的后续回复。
|
||||
要求:
|
||||
1. 字数 {word_min}~{word_max} 字
|
||||
2. 语气像真实用户交流,可以认同、补充或追问
|
||||
3. 必须围绕文章和评论内容,不要跑题
|
||||
4. 只输出回复正文,不要加任何前缀或解释
|
||||
|
||||
回复:"""
|
||||
return await self._call_api(db, prompt, system_prompt, max_tokens=180)
|
||||
|
||||
async def test_model(self, db: AsyncSession, model_id: int, test_prompt: str) -> dict:
|
||||
"""测试模型可用性"""
|
||||
result = await db.execute(select(AIModelConfig).where(AIModelConfig.id == model_id))
|
||||
|
||||
@@ -7,7 +7,7 @@ import uuid
|
||||
import hashlib
|
||||
import hmac
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from typing import Optional, Any
|
||||
import httpx
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update
|
||||
@@ -45,6 +45,31 @@ class NewsPlatformService:
|
||||
"orgId": await self._cfg(db, "platform_org_id", ""),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _api_root(base_url: str) -> str:
|
||||
if "/api/" in base_url:
|
||||
return base_url.split("/api/", 1)[0] + "/api"
|
||||
return base_url.rstrip("/")
|
||||
|
||||
@staticmethod
|
||||
def _resolve_synced_nickname(user: VirtualUser, sync_nickname: str, sync_real_name: str) -> str:
|
||||
"""避免登录同步把有意义的本地昵称覆盖回手机号。"""
|
||||
current_nickname = (user.nickname or "").strip()
|
||||
current_real_name = (user.real_name or "").strip()
|
||||
sync_nickname = (sync_nickname or "").strip()
|
||||
sync_real_name = (sync_real_name or "").strip()
|
||||
account = (user.account or "").strip()
|
||||
|
||||
if current_nickname and current_nickname != account:
|
||||
return current_nickname
|
||||
if sync_nickname and sync_nickname != account:
|
||||
return sync_nickname
|
||||
if sync_real_name:
|
||||
return sync_real_name
|
||||
if current_real_name:
|
||||
return current_real_name
|
||||
return sync_nickname or current_nickname or account
|
||||
|
||||
# ─── 签名(完全对应 sign.js 逻辑) ─────────────────────────
|
||||
@staticmethod
|
||||
def _make_sign(params: dict, secret_key: str, sign_type: str = "MD5") -> str:
|
||||
@@ -188,6 +213,7 @@ class NewsPlatformService:
|
||||
sync_real_name = user_info.get("realName") or ""
|
||||
sync_sex = int(user_info.get("sex") or 0)
|
||||
sync_avatar = user_info.get("avatar") or ""
|
||||
preferred_nickname = self._resolve_synced_nickname(user, sync_nickname, sync_real_name)
|
||||
|
||||
await set_session(user.id, {
|
||||
"token": access_token,
|
||||
@@ -196,7 +222,7 @@ class NewsPlatformService:
|
||||
"org_id": org_id or cfg.get("orgId", ""),
|
||||
"login_time": datetime.now().isoformat(),
|
||||
# 缓存用户信息供 sync 使用
|
||||
"nickname": sync_nickname,
|
||||
"nickname": preferred_nickname,
|
||||
"real_name": sync_real_name,
|
||||
"sex": sync_sex,
|
||||
"avatar": sync_avatar,
|
||||
@@ -209,7 +235,7 @@ class NewsPlatformService:
|
||||
last_login_at=datetime.now(),
|
||||
platform_uid=platform_uid,
|
||||
)
|
||||
if sync_nickname: update_vals["nickname"] = sync_nickname
|
||||
if preferred_nickname: update_vals["nickname"] = preferred_nickname
|
||||
if sync_real_name: update_vals["real_name"] = sync_real_name
|
||||
if sync_sex: update_vals["sex"] = sync_sex
|
||||
if sync_avatar: update_vals["avatar_url"] = sync_avatar
|
||||
@@ -719,51 +745,287 @@ class NewsPlatformService:
|
||||
return False
|
||||
|
||||
async def post_comment(self, db, user, news_id, news_title, content, news_author_id="", org_id="") -> tuple[bool, str]:
|
||||
success, err, _ = await self.post_comment_with_record_id(
|
||||
db, user, news_id, news_title, content,
|
||||
news_author_id=news_author_id, org_id=org_id,
|
||||
)
|
||||
return success, err
|
||||
|
||||
async def post_comment_with_record_id(
|
||||
self, db, user, news_id, news_title, content, news_author_id="", org_id=""
|
||||
) -> tuple[bool, str, str]:
|
||||
sess = await get_session(user.id)
|
||||
if not sess:
|
||||
return False, "未登录"
|
||||
return False, "未登录", ""
|
||||
biz = await self._biz_url(db)
|
||||
cfg = await self._client(db)
|
||||
uid = sess.get("platform_uid", "")
|
||||
# org_id 优先取文章自带的(从广场数据获取),否则取 session/配置
|
||||
final_org_id = org_id or sess.get("org_id") or cfg.get("orgId") or ""
|
||||
if not final_org_id:
|
||||
final_org_id = await self._get_article_org_id(db, sess["token"], news_id)
|
||||
body = {
|
||||
"module": "news", "topicId": news_id, "title": news_title,
|
||||
"content": content, "orgId": final_org_id,
|
||||
"toUserId": news_author_id or uid, "userId": uid,
|
||||
"userName": user.nickname, "avatar": user.avatar_url or "",
|
||||
}
|
||||
return await self._json_post(f"{biz}/message/comment", self._bearer(sess["token"]), body)
|
||||
success, err, record_id = await self._json_post_with_record_id(
|
||||
f"{biz}/message/comment", self._bearer(sess["token"]), body
|
||||
)
|
||||
if success and not record_id:
|
||||
record_id, _ = await self.find_comment_id(db, user, news_id, content)
|
||||
return success, err, record_id
|
||||
|
||||
async def post_reply(self, db, user, news_id, comment_id, content) -> tuple[bool, str]:
|
||||
async def post_reply(
|
||||
self, db, user, news_id, comment_id, content,
|
||||
parent_comment: dict | None = None,
|
||||
reply_to: dict | None = None,
|
||||
article_title: str = "",
|
||||
org_id: str = "",
|
||||
) -> tuple[bool, str]:
|
||||
success, err, _ = await self.post_reply_with_record_id(
|
||||
db, user, news_id, comment_id, content,
|
||||
parent_comment=parent_comment,
|
||||
reply_to=reply_to,
|
||||
article_title=article_title,
|
||||
org_id=org_id,
|
||||
)
|
||||
return success, err
|
||||
|
||||
async def post_reply_with_record_id(
|
||||
self, db, user, news_id, comment_id, content,
|
||||
parent_comment: dict | None = None,
|
||||
reply_to: dict | None = None,
|
||||
article_title: str = "",
|
||||
org_id: str = "",
|
||||
) -> tuple[bool, str, str]:
|
||||
sess = await get_session(user.id)
|
||||
if not sess:
|
||||
return False, "未登录"
|
||||
return False, "未登录", ""
|
||||
biz = await self._biz_url(db)
|
||||
uid = sess.get("platform_uid", "")
|
||||
cfg = await self._client(db)
|
||||
parent_comment = parent_comment or {}
|
||||
reply_to = reply_to or None
|
||||
parent_comment_id = str(parent_comment.get("id") or parent_comment.get("commentId") or comment_id or "")
|
||||
parent_user_id = str(
|
||||
parent_comment.get("createUser")
|
||||
or parent_comment.get("userId")
|
||||
or parent_comment.get("commentUserId")
|
||||
or parent_comment.get("fromUserId")
|
||||
or ""
|
||||
)
|
||||
parent_user_name = (
|
||||
parent_comment.get("userName")
|
||||
or parent_comment.get("fromUserName")
|
||||
or parent_comment.get("nickName")
|
||||
or ""
|
||||
)
|
||||
reply_target = reply_to or parent_comment
|
||||
reply_target_id = str(
|
||||
reply_target.get("id")
|
||||
or reply_target.get("replyId")
|
||||
or reply_target.get("commentId")
|
||||
or parent_comment_id
|
||||
)
|
||||
reply_target_user_id = str(
|
||||
reply_target.get("createUser")
|
||||
or reply_target.get("fromUserId")
|
||||
or reply_target.get("userId")
|
||||
or reply_target.get("commentUserId")
|
||||
or parent_user_id
|
||||
or ""
|
||||
)
|
||||
reply_target_user_name = (
|
||||
reply_target.get("fromUserName")
|
||||
or reply_target.get("userName")
|
||||
or reply_target.get("nickName")
|
||||
or parent_user_name
|
||||
or ""
|
||||
)
|
||||
from_user_name = (
|
||||
(user.real_name or "").strip()
|
||||
or (user.nickname or "").strip()
|
||||
or (user.account or "").strip()
|
||||
)
|
||||
final_org_id = org_id or sess.get("org_id") or cfg.get("orgId") or ""
|
||||
if not final_org_id:
|
||||
final_org_id = await self._get_article_org_id(db, sess["token"], news_id)
|
||||
body = {
|
||||
"module": "news", "topicId": news_id, "commentId": comment_id,
|
||||
"commentUserId": uid, "content": content,
|
||||
"fromUserName": user.nickname, "avatar": user.avatar_url or "",
|
||||
"module": "news",
|
||||
"modules": "news",
|
||||
"topicId": news_id,
|
||||
"commentId": parent_comment_id,
|
||||
"commentUserId": parent_user_id,
|
||||
"replyId": reply_target_id,
|
||||
"toUserId": reply_target_user_id,
|
||||
"replyType": "2" if reply_to else "1",
|
||||
"toUserName": reply_target_user_name,
|
||||
"content": content,
|
||||
"title": article_title or parent_comment.get("title") or "",
|
||||
"orgId": final_org_id,
|
||||
"fromUserName": from_user_name,
|
||||
"avatar": user.avatar_url or "",
|
||||
}
|
||||
return await self._json_post(f"{biz}/message/comment/reply", self._bearer(sess["token"]), body)
|
||||
success, err, reply_id = await self._json_post_with_record_id(
|
||||
f"{biz}/message/comment/reply", self._bearer(sess["token"]), body
|
||||
)
|
||||
if success and not reply_id:
|
||||
reply_id, _ = await self.find_reply_id(
|
||||
db, user, news_id,
|
||||
content=content,
|
||||
parent_comment_id=parent_comment_id,
|
||||
)
|
||||
return success, err, reply_id
|
||||
|
||||
async def _get_article_org_id(self, db, token: str, news_id: str) -> str:
|
||||
"""从文章详情补齐 H5 评论/回复所需的组织上下文。"""
|
||||
if not news_id:
|
||||
return ""
|
||||
biz = await self._biz_url(db)
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=8) as c:
|
||||
r = await c.get(f"{biz}/news/{news_id}", headers=self._bearer(token))
|
||||
if r.status_code != 200:
|
||||
return ""
|
||||
data = (r.json().get("data") or {})
|
||||
return str(
|
||||
data.get("orgId")
|
||||
or data.get("alumnusId")
|
||||
or data.get("publishOrgId")
|
||||
or data.get("initialPublishOrgId")
|
||||
or ""
|
||||
)
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
async def get_comments(self, db, user, news_id) -> list:
|
||||
data = await self.get_comments_payload(db, user, news_id)
|
||||
if isinstance(data, dict):
|
||||
rows = data.get("data") or data.get("records") or data.get("list") or data.get("rows")
|
||||
return rows if isinstance(rows, list) else []
|
||||
return data if isinstance(data, list) else []
|
||||
|
||||
async def get_comments_payload(self, db, user, news_id) -> Any:
|
||||
sess = await get_session(user.id)
|
||||
if not sess:
|
||||
return []
|
||||
biz = await self._biz_url(db)
|
||||
cfg = await self._client(db)
|
||||
try:
|
||||
params = self._build_form({"module": "news", "topicId": news_id, "pageNum": 1, "pageSize": 20}, cfg)
|
||||
params = {"module": "news", "topicId": news_id, "pageNum": 1, "pageSize": 20}
|
||||
async with httpx.AsyncClient(timeout=10) as c:
|
||||
r = await c.get(f"{biz}/message/comment", headers=self._bearer(sess["token"]), params=params)
|
||||
r = await c.get(
|
||||
f"{self._api_root(biz)}/interaction/open/selectInteractionCommentList",
|
||||
headers=self._bearer(sess["token"]),
|
||||
params=params,
|
||||
)
|
||||
if r.status_code == 200:
|
||||
return r.json().get("data", {}).get("data") or []
|
||||
return r.json().get("data") or []
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
async def find_comment_id(self, db, user, news_id: str, content: str = "") -> tuple[str, int | None]:
|
||||
"""Best-effort lookup for old records created before platform IDs were saved."""
|
||||
sess = await get_session(user.id)
|
||||
my_uid = str(sess.get("platform_uid", "")) if sess else ""
|
||||
payload = await self.get_comments_payload(db, user, news_id)
|
||||
comment_count = None
|
||||
comments = payload if isinstance(payload, list) else []
|
||||
if isinstance(payload, dict):
|
||||
try:
|
||||
comment_count = int(payload.get("commentCount"))
|
||||
except (TypeError, ValueError):
|
||||
comment_count = None
|
||||
comments = (
|
||||
payload.get("data")
|
||||
or payload.get("records")
|
||||
or payload.get("list")
|
||||
or payload.get("rows")
|
||||
or []
|
||||
)
|
||||
if not isinstance(comments, list):
|
||||
comments = []
|
||||
|
||||
target_content = (content or "").strip()
|
||||
for item in comments:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
author_id = str(item.get("userId") or item.get("createUser") or item.get("commentUserId") or "")
|
||||
item_content = (item.get("content") or "").strip()
|
||||
if my_uid and author_id and author_id != my_uid:
|
||||
continue
|
||||
if target_content and item_content != target_content:
|
||||
continue
|
||||
comment_id = str(item.get("id") or item.get("commentId") or item.get("messageId") or "")
|
||||
if comment_id:
|
||||
return comment_id, comment_count
|
||||
return "", comment_count
|
||||
|
||||
async def find_reply_id(
|
||||
self, db, user, news_id: str, content: str = "", parent_comment_id: str = ""
|
||||
) -> tuple[str, int | None]:
|
||||
"""Best-effort lookup for reply records created before platform IDs were saved."""
|
||||
sess = await get_session(user.id)
|
||||
my_uid = str(sess.get("platform_uid", "")) if sess else ""
|
||||
payload = await self.get_comments_payload(db, user, news_id)
|
||||
reply_count = None
|
||||
comments = payload if isinstance(payload, list) else []
|
||||
if isinstance(payload, dict):
|
||||
for count_key in ("replyCount", "commentCount", "total"):
|
||||
try:
|
||||
if payload.get(count_key) is not None:
|
||||
reply_count = int(payload.get(count_key))
|
||||
break
|
||||
except (TypeError, ValueError):
|
||||
reply_count = None
|
||||
comments = (
|
||||
payload.get("data")
|
||||
or payload.get("records")
|
||||
or payload.get("list")
|
||||
or payload.get("rows")
|
||||
or []
|
||||
)
|
||||
if not isinstance(comments, list):
|
||||
comments = []
|
||||
|
||||
target_content = (content or "").strip()
|
||||
target_parent_id = str(parent_comment_id or "")
|
||||
for item in comments:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
current_parent_id = str(item.get("id") or item.get("commentId") or "")
|
||||
if target_parent_id and current_parent_id != target_parent_id:
|
||||
continue
|
||||
replies = (
|
||||
item.get("respList")
|
||||
or item.get("replyList")
|
||||
or item.get("children")
|
||||
or item.get("replies")
|
||||
or []
|
||||
)
|
||||
if not isinstance(replies, list):
|
||||
continue
|
||||
for reply in replies:
|
||||
if not isinstance(reply, dict):
|
||||
continue
|
||||
author_id = str(
|
||||
reply.get("fromUserId")
|
||||
or reply.get("createUser")
|
||||
or reply.get("userId")
|
||||
or reply.get("commentUserId")
|
||||
or ""
|
||||
)
|
||||
reply_content = (reply.get("content") or "").strip()
|
||||
if my_uid and author_id and author_id != my_uid:
|
||||
continue
|
||||
if target_content and reply_content != target_content:
|
||||
continue
|
||||
reply_id = str(reply.get("id") or reply.get("replyId") or reply.get("commentId") or "")
|
||||
if reply_id:
|
||||
return reply_id, reply_count
|
||||
return "", reply_count
|
||||
|
||||
async def like_news(self, db, user, news_id, org_id="", to_user_id="", title="") -> tuple[bool, str]:
|
||||
sess = await get_session(user.id)
|
||||
if not sess:
|
||||
@@ -825,13 +1087,41 @@ class NewsPlatformService:
|
||||
return True, ""
|
||||
return False, f"HTTP {resp.status_code}"
|
||||
|
||||
@staticmethod
|
||||
def _extract_record_id(data: Any) -> str:
|
||||
if data is None:
|
||||
return ""
|
||||
if isinstance(data, (str, int)):
|
||||
return str(data)
|
||||
if isinstance(data, dict):
|
||||
for key in ("id", "replyId", "commentId", "messageId", "recordId"):
|
||||
if data.get(key):
|
||||
return str(data[key])
|
||||
for key in ("data", "record", "reply", "comment", "message"):
|
||||
nested = NewsPlatformService._extract_record_id(data.get(key))
|
||||
if nested:
|
||||
return nested
|
||||
return ""
|
||||
|
||||
async def _json_post(self, url, headers, body) -> tuple[bool, str]:
|
||||
success, err, _ = await self._json_post_with_record_id(url, headers, body)
|
||||
return success, err
|
||||
|
||||
async def _json_post_with_record_id(self, url, headers, body) -> tuple[bool, str, str]:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15) as c:
|
||||
r = await c.post(url, json=body, headers=headers)
|
||||
return self._ok(r)
|
||||
if r.status_code not in [200, 201]:
|
||||
return False, f"HTTP {r.status_code}", ""
|
||||
try:
|
||||
d = r.json()
|
||||
except Exception:
|
||||
return True, "", ""
|
||||
if d.get("code") in [0, 200]:
|
||||
return True, "", self._extract_record_id(d.get("data"))
|
||||
return False, d.get("message") or "业务失败", ""
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
return False, str(e), ""
|
||||
|
||||
async def _write_login_log(self, db, user, action, session_id=None, error_msg=None):
|
||||
try:
|
||||
@@ -898,6 +1188,25 @@ class NewsPlatformService:
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
async def cancel_reply(self, db, user, news_id: str, reply_id: str) -> tuple[bool, str]:
|
||||
"""DELETE /message/comment/reply/{topicId}/{id} 删除评论回复"""
|
||||
sess = await get_session(user.id)
|
||||
if not sess:
|
||||
return False, "未登录"
|
||||
biz = await self._biz_url(db)
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as c:
|
||||
r = await c.delete(
|
||||
f"{biz}/message/comment/reply/{news_id}/{reply_id}",
|
||||
headers=self._bearer(sess["token"]),
|
||||
)
|
||||
d = r.json()
|
||||
if d.get("code") in [0, 200]:
|
||||
return True, ""
|
||||
return False, d.get("message", "删除回复失败")
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
async def cancel_collect(self, db, user, news_id: str, org_id: str = "", to_user_id: str = "", title: str = "") -> tuple[bool, str]:
|
||||
"""取消收藏(复用取消点赞接口)"""
|
||||
return await self.cancel_like(db, user, news_id, org_id=org_id, to_user_id=to_user_id, title=title)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""调度服务 - 定时自动互动、会话校验"""
|
||||
import random
|
||||
import asyncio
|
||||
from datetime import datetime, date
|
||||
from datetime import datetime, date, timedelta
|
||||
from typing import Optional
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
@@ -9,7 +9,7 @@ from sqlalchemy import select, update, func
|
||||
|
||||
from app.core.database import AsyncSessionLocal
|
||||
from app.core.logger import logger
|
||||
from app.models import VirtualUser, UserPersonality, InteractionRecord, SystemConfig
|
||||
from app.models import VirtualUser, UserPersonality, InteractionRecord, PendingReplyTask, SystemConfig
|
||||
|
||||
|
||||
class SchedulerService:
|
||||
@@ -23,6 +23,12 @@ class SchedulerService:
|
||||
from app.core.database import AsyncSessionLocal
|
||||
logger.info("⚡ 立即触发互动任务")
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
max_concurrent = int(await self._get_config(session, "max_concurrent_users", "5"))
|
||||
except (TypeError, ValueError):
|
||||
max_concurrent = 5
|
||||
max_concurrent = max(1, max_concurrent)
|
||||
|
||||
result_r = await session.execute(
|
||||
select(VirtualUser).where(
|
||||
VirtualUser.status == 2,
|
||||
@@ -31,13 +37,62 @@ class SchedulerService:
|
||||
)
|
||||
users = result_r.scalars().all()
|
||||
if not users:
|
||||
return {"message": "没有已登录的用户", "triggered": 0}
|
||||
return {
|
||||
"message": "没有已登录的用户",
|
||||
"requested_concurrency": max_concurrent,
|
||||
"attempted_count": 0,
|
||||
"success_count": 0,
|
||||
"skipped_count": 0,
|
||||
"failed_count": 0,
|
||||
"triggered": 0,
|
||||
"users": [],
|
||||
"results": [],
|
||||
}
|
||||
import random
|
||||
selected = random.sample(users, min(5, len(users)))
|
||||
selected = random.sample(users, min(max_concurrent, len(users)))
|
||||
import asyncio
|
||||
tasks = [self._execute_user_interaction(u.id) for u in selected]
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
return {"triggered": len(selected), "users": [u.account for u in selected]}
|
||||
raw_results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
results = []
|
||||
success_count = skipped_count = failed_count = 0
|
||||
for user, outcome in zip(selected, raw_results):
|
||||
if isinstance(outcome, Exception):
|
||||
item = {
|
||||
"user_id": user.id,
|
||||
"account": user.account,
|
||||
"status": "failed",
|
||||
"reason": str(outcome),
|
||||
"interactions": [],
|
||||
}
|
||||
failed_count += 1
|
||||
else:
|
||||
item = outcome or {
|
||||
"user_id": user.id,
|
||||
"account": user.account,
|
||||
"status": "skipped",
|
||||
"reason": "no_result",
|
||||
"interactions": [],
|
||||
}
|
||||
status = item.get("status")
|
||||
if status == "success":
|
||||
success_count += 1
|
||||
elif status == "failed":
|
||||
failed_count += 1
|
||||
else:
|
||||
skipped_count += 1
|
||||
results.append(item)
|
||||
|
||||
return {
|
||||
"requested_concurrency": max_concurrent,
|
||||
"attempted_count": len(selected),
|
||||
"success_count": success_count,
|
||||
"skipped_count": skipped_count,
|
||||
"failed_count": failed_count,
|
||||
"triggered": len(selected),
|
||||
"users": [u.account for u in selected],
|
||||
"results": results,
|
||||
}
|
||||
|
||||
async def start(self):
|
||||
if self._running:
|
||||
@@ -52,6 +107,11 @@ class SchedulerService:
|
||||
self._run_interactions, IntervalTrigger(minutes=5),
|
||||
id="run_interactions", replace_existing=True
|
||||
)
|
||||
# 待发送回复队列:持久化延迟回复,后端重启后可继续发送
|
||||
self.scheduler.add_job(
|
||||
self._process_pending_reply_tasks, IntervalTrigger(seconds=30),
|
||||
id="process_pending_reply_tasks", replace_existing=True
|
||||
)
|
||||
# 每日零点重置计数
|
||||
self.scheduler.add_job(
|
||||
self._daily_reset, "cron", hour=16, minute=0, # 北京时间 00:00 = UTC 16:00
|
||||
@@ -221,7 +281,13 @@ class SchedulerService:
|
||||
user_result = await db.execute(select(VirtualUser).where(VirtualUser.id == user_id))
|
||||
user = user_result.scalar_one_or_none()
|
||||
if not user or user.status != 2:
|
||||
return
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"account": getattr(user, "account", ""),
|
||||
"status": "skipped",
|
||||
"reason": "user_not_logged_in",
|
||||
"interactions": [],
|
||||
}
|
||||
|
||||
# 检查今日评论限额
|
||||
can_comment = True
|
||||
@@ -255,7 +321,13 @@ class SchedulerService:
|
||||
f"用户 {user.account} 获取新闻列表为空 "
|
||||
f"(orgId={await news_service._cfg(db, 'platform_org_id', '')})"
|
||||
)
|
||||
return
|
||||
return {
|
||||
"user_id": user.id,
|
||||
"account": user.account,
|
||||
"status": "skipped",
|
||||
"reason": "no_articles",
|
||||
"interactions": [],
|
||||
}
|
||||
|
||||
# ── 文章去重 + 热度加权选取 ─────────────────────────────────
|
||||
# 查询今日已互动过的文章(所有类型),避免重复互动同一篇
|
||||
@@ -307,7 +379,14 @@ class SchedulerService:
|
||||
article_org_id = str(article.get("orgId") or "")
|
||||
|
||||
if not news_id:
|
||||
return
|
||||
return {
|
||||
"user_id": user.id,
|
||||
"account": user.account,
|
||||
"status": "skipped",
|
||||
"reason": "missing_news_id",
|
||||
"interactions": [],
|
||||
"article_title": news_title,
|
||||
}
|
||||
|
||||
# 读取互动概率
|
||||
comment_prob = float(await self._get_config_from_db(db, "comment_probability", "0.4"))
|
||||
@@ -317,6 +396,7 @@ class SchedulerService:
|
||||
forward_prob = float(await self._get_config_from_db(db, "forward_probability", "0.15"))
|
||||
|
||||
interactions_done = []
|
||||
action_failures = []
|
||||
|
||||
# ① 先记录阅读(每次必做,模拟真实用户打开文章)
|
||||
await news_service.read_news(db, user, news_id)
|
||||
@@ -331,6 +411,8 @@ class SchedulerService:
|
||||
if success:
|
||||
interactions_done.append("like")
|
||||
await self._incr_total(db, user_id)
|
||||
else:
|
||||
action_failures.append({"type": "like", "error": err})
|
||||
|
||||
# ③ 收藏(每篇文章每用户每天只收藏一次)
|
||||
if "collect" not in done_on_this and random.random() < collect_prob:
|
||||
@@ -338,6 +420,8 @@ class SchedulerService:
|
||||
await self._save_record(db, user, news_id, news_title, "collect", None, 0, success, err)
|
||||
if success:
|
||||
interactions_done.append("collect")
|
||||
else:
|
||||
action_failures.append({"type": "collect", "error": err})
|
||||
|
||||
# ④ 转发(每篇文章每用户每天只转发一次)
|
||||
if "forward" not in done_on_this and random.random() < forward_prob:
|
||||
@@ -346,109 +430,433 @@ class SchedulerService:
|
||||
if success:
|
||||
interactions_done.append("forward")
|
||||
await self._incr_total(db, user_id)
|
||||
else:
|
||||
action_failures.append({"type": "forward", "error": err})
|
||||
|
||||
# ⑤ 评论/回复逻辑(去重:已评论过的文章改为回复他人评论)
|
||||
# ⑤ 评论/回复逻辑:评论和回复互相独立,未评论过文章也可以回复他人评论
|
||||
if can_comment and personality:
|
||||
style_prompt = personality.comment_style_prompt or ""
|
||||
safe_word_max = min(personality.word_count_max, 80)
|
||||
|
||||
if already_commented_this:
|
||||
# 已评论过此文章 → 改为回复其他用户的评论(虚拟用户互动)
|
||||
if random.random() < reply_prob:
|
||||
existing = await news_service.get_comments(db, user, news_id)
|
||||
if existing:
|
||||
# 优先回复虚拟用户的评论(促进互动),过滤掉自己的评论
|
||||
from app.core.redis_client import get_session as _gs
|
||||
my_sess = await _gs(user.id)
|
||||
my_uid = my_sess.get("platform_uid", "") if my_sess else ""
|
||||
others = [c for c in existing
|
||||
if str(c.get("userId") or c.get("createUser") or "") != my_uid]
|
||||
if others:
|
||||
target = random.choice(others)
|
||||
cid = str(target.get("id") or target.get("commentId") or "")
|
||||
parent_content = target.get("content") or ""
|
||||
if cid:
|
||||
reply_text, r_tokens = await ai_service.generate_reply(
|
||||
db, news_title, parent_content,
|
||||
style_prompt,
|
||||
personality.word_count_min,
|
||||
safe_word_max
|
||||
)
|
||||
if reply_text:
|
||||
r_ok, r_err = await news_service.post_reply(
|
||||
db, user, news_id, cid, reply_text
|
||||
)
|
||||
await self._save_record(
|
||||
db, user, news_id, news_title, "reply",
|
||||
reply_text, r_tokens, r_ok, r_err,
|
||||
parent_comment_id=cid
|
||||
)
|
||||
if r_ok:
|
||||
interactions_done.append("reply")
|
||||
logger.info(f"💬 {user.account} 回复了已评论文章的评论(去重逻辑)")
|
||||
else:
|
||||
# 未评论过此文章 → 正常发评论
|
||||
if random.random() < comment_prob:
|
||||
comment_text, tokens = await ai_service.generate_comment(
|
||||
db, news_title, news_content,
|
||||
style_prompt, personality.word_count_min, safe_word_max
|
||||
)
|
||||
if comment_text:
|
||||
success, err = await news_service.post_comment(
|
||||
db, user, news_id, news_title, comment_text,
|
||||
news_author_id=news_author, org_id=article_org_id
|
||||
)
|
||||
await self._save_record(
|
||||
db, user, news_id, news_title, "comment",
|
||||
comment_text, tokens, success, err
|
||||
)
|
||||
if success:
|
||||
interactions_done.append("comment")
|
||||
await db.execute(
|
||||
update(VirtualUser).where(VirtualUser.id == user_id).values(
|
||||
today_comment_count=VirtualUser.today_comment_count + 1,
|
||||
total_interactions=VirtualUser.total_interactions + 1,
|
||||
last_interact_at=datetime.now()
|
||||
)
|
||||
)
|
||||
if random.random() < reply_prob:
|
||||
reply_actions, reply_failures = await self._run_reply_interaction_chain(
|
||||
db=db,
|
||||
starter=user,
|
||||
starter_personality=personality,
|
||||
news_service=news_service,
|
||||
ai_service=ai_service,
|
||||
news_id=news_id,
|
||||
news_title=news_title,
|
||||
article_org_id=article_org_id,
|
||||
style_prompt=style_prompt,
|
||||
safe_word_max=safe_word_max,
|
||||
)
|
||||
interactions_done.extend(reply_actions)
|
||||
action_failures.extend(reply_failures)
|
||||
|
||||
# ⑥ 评论成功后,随机回复其他用户的评论(互动链)
|
||||
if random.random() < reply_prob:
|
||||
existing = await news_service.get_comments(db, user, news_id)
|
||||
if existing:
|
||||
from app.core.redis_client import get_session as _gs2
|
||||
my_sess2 = await _gs2(user.id)
|
||||
my_uid2 = my_sess2.get("platform_uid", "") if my_sess2 else ""
|
||||
others2 = [c for c in existing
|
||||
if str(c.get("userId") or c.get("createUser") or "") != my_uid2]
|
||||
if others2:
|
||||
target2 = random.choice(others2)
|
||||
cid2 = str(target2.get("id") or target2.get("commentId") or "")
|
||||
parent_content2 = target2.get("content") or ""
|
||||
if cid2:
|
||||
reply_text2, r_tokens2 = await ai_service.generate_reply(
|
||||
db, news_title, parent_content2,
|
||||
style_prompt,
|
||||
personality.word_count_min,
|
||||
safe_word_max
|
||||
)
|
||||
if reply_text2:
|
||||
r_ok2, r_err2 = await news_service.post_reply(
|
||||
db, user, news_id, cid2, reply_text2
|
||||
)
|
||||
await self._save_record(
|
||||
db, user, news_id, news_title, "reply",
|
||||
reply_text2, r_tokens2, r_ok2, r_err2,
|
||||
parent_comment_id=cid2
|
||||
)
|
||||
if r_ok2:
|
||||
interactions_done.append("reply")
|
||||
# 每篇文章每个用户每天只发一条顶层评论;回复不再要求先评论
|
||||
if not already_commented_this and random.random() < comment_prob:
|
||||
comment_text, tokens = await ai_service.generate_comment(
|
||||
db, news_title, news_content,
|
||||
style_prompt, personality.word_count_min, safe_word_max
|
||||
)
|
||||
if comment_text:
|
||||
success, err, comment_record_id = await news_service.post_comment_with_record_id(
|
||||
db, user, news_id, news_title, comment_text,
|
||||
news_author_id=news_author, org_id=article_org_id
|
||||
)
|
||||
await self._save_record(
|
||||
db, user, news_id, news_title, "comment",
|
||||
comment_text, tokens, success, err,
|
||||
platform_record_id=comment_record_id,
|
||||
)
|
||||
if success:
|
||||
interactions_done.append("comment")
|
||||
await db.execute(
|
||||
update(VirtualUser).where(VirtualUser.id == user_id).values(
|
||||
today_comment_count=VirtualUser.today_comment_count + 1,
|
||||
total_interactions=VirtualUser.total_interactions + 1,
|
||||
last_interact_at=datetime.now()
|
||||
)
|
||||
)
|
||||
else:
|
||||
action_failures.append({"type": "comment", "error": err})
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"👤 {user.account} 互动完成: {interactions_done} [新闻: {news_title[:20]}]")
|
||||
if interactions_done:
|
||||
return {
|
||||
"user_id": user.id,
|
||||
"account": user.account,
|
||||
"status": "success",
|
||||
"reason": "",
|
||||
"interactions": interactions_done,
|
||||
"article_id": news_id,
|
||||
"article_title": news_title,
|
||||
}
|
||||
if action_failures:
|
||||
return {
|
||||
"user_id": user.id,
|
||||
"account": user.account,
|
||||
"status": "failed",
|
||||
"reason": "; ".join(
|
||||
f"{item['type']}:{item['error'] or 'unknown'}" for item in action_failures
|
||||
),
|
||||
"interactions": [],
|
||||
"article_id": news_id,
|
||||
"article_title": news_title,
|
||||
}
|
||||
return {
|
||||
"user_id": user.id,
|
||||
"account": user.account,
|
||||
"status": "skipped",
|
||||
"reason": "no_actions_triggered",
|
||||
"interactions": [],
|
||||
"article_id": news_id,
|
||||
"article_title": news_title,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"用户 {user_id} 互动异常: {e}")
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"account": "",
|
||||
"status": "failed",
|
||||
"reason": str(e),
|
||||
"interactions": [],
|
||||
}
|
||||
|
||||
async def _run_reply_interaction_chain(
|
||||
self,
|
||||
db,
|
||||
starter: VirtualUser,
|
||||
starter_personality,
|
||||
news_service,
|
||||
ai_service,
|
||||
news_id: str,
|
||||
news_title: str,
|
||||
article_org_id: str,
|
||||
style_prompt: str,
|
||||
safe_word_max: int,
|
||||
) -> tuple[list[str], list[dict]]:
|
||||
"""主动回复评论,并按概率安排后续延迟回应。"""
|
||||
from app.core.redis_client import get_session
|
||||
|
||||
actions: list[str] = []
|
||||
failures: list[dict] = []
|
||||
starter_sess = await get_session(starter.id)
|
||||
starter_uid = str(starter_sess.get("platform_uid") or "") if starter_sess else ""
|
||||
comments = await news_service.get_comments(db, starter, news_id)
|
||||
if not comments:
|
||||
return actions, failures
|
||||
|
||||
candidates = [
|
||||
c for c in comments
|
||||
if str(c.get("id") or c.get("commentId") or "")
|
||||
and (c.get("content") or "").strip()
|
||||
and str(c.get("userId") or c.get("createUser") or "") != starter_uid
|
||||
]
|
||||
if not candidates:
|
||||
return actions, failures
|
||||
|
||||
parent_comment = random.choice(candidates)
|
||||
parent_comment_id = str(parent_comment.get("id") or parent_comment.get("commentId") or "")
|
||||
root_content = (parent_comment.get("content") or "").strip()
|
||||
context = f"准备回复这条评论:{root_content}"
|
||||
reply_text, reply_tokens = await ai_service.generate_thread_reply(
|
||||
db, news_title, root_content, context,
|
||||
style_prompt,
|
||||
starter_personality.word_count_min,
|
||||
safe_word_max,
|
||||
)
|
||||
if not reply_text:
|
||||
return actions, failures
|
||||
|
||||
ok, err, reply_id = await news_service.post_reply_with_record_id(
|
||||
db, starter, news_id, parent_comment_id, reply_text,
|
||||
parent_comment=parent_comment,
|
||||
article_title=news_title,
|
||||
org_id=article_org_id,
|
||||
)
|
||||
await self._save_record(
|
||||
db, starter, news_id, news_title, "reply",
|
||||
reply_text, reply_tokens, ok, err,
|
||||
parent_comment_id=parent_comment_id,
|
||||
platform_record_id=reply_id,
|
||||
)
|
||||
if not ok:
|
||||
failures.append({"type": "reply", "error": err})
|
||||
return actions, failures
|
||||
|
||||
actions.append("reply")
|
||||
await self._incr_total(db, starter.id)
|
||||
logger.info(f"💬 {starter.account} 回复了文章评论")
|
||||
|
||||
starter_reply = {
|
||||
"id": reply_id,
|
||||
"content": reply_text,
|
||||
"createUser": starter_uid,
|
||||
"fromUserName": starter.real_name or starter.nickname or starter.account,
|
||||
}
|
||||
|
||||
chain_probability = await self._get_float_config(db, "reply_chain_probability", 0.3)
|
||||
delay_min = await self._get_int_config(db, "reply_chain_delay_min_seconds", 30)
|
||||
delay_max = await self._get_int_config(db, "reply_chain_delay_max_seconds", 7200)
|
||||
if delay_max < delay_min:
|
||||
delay_max = delay_min
|
||||
|
||||
# 评论作者如果也是当前系统里的已登录虚拟用户,按概率安排稍后回复这条回复。
|
||||
parent_author_uid = str(parent_comment.get("createUser") or parent_comment.get("userId") or "")
|
||||
parent_author = await self._get_logged_in_user_by_platform_uid(db, parent_author_uid)
|
||||
if (
|
||||
parent_author
|
||||
and parent_author.id != starter.id
|
||||
and random.random() < chain_probability
|
||||
):
|
||||
delay_seconds = random.randint(delay_min, delay_max)
|
||||
await self._enqueue_pending_reply_task(
|
||||
db=db,
|
||||
delay_seconds=delay_seconds,
|
||||
actor_id=parent_author.id,
|
||||
news_id=news_id,
|
||||
news_title=news_title,
|
||||
article_org_id=article_org_id,
|
||||
parent_comment=parent_comment,
|
||||
reply_to=starter_reply,
|
||||
root_content=root_content,
|
||||
context=f"对方刚回复了你的评论:{reply_text}",
|
||||
next_actor_id=starter.id,
|
||||
next_probability=chain_probability,
|
||||
next_delay_min=delay_min,
|
||||
next_delay_max=delay_max,
|
||||
)
|
||||
logger.info(
|
||||
f"⏳ {parent_author.account} 已进入待发送回复队列,延迟 {delay_seconds}s 后发送"
|
||||
)
|
||||
|
||||
return actions, failures
|
||||
|
||||
async def _enqueue_pending_reply_task(
|
||||
self,
|
||||
db,
|
||||
delay_seconds: int,
|
||||
actor_id: int,
|
||||
news_id: str,
|
||||
news_title: str,
|
||||
article_org_id: str,
|
||||
parent_comment: dict,
|
||||
reply_to: dict,
|
||||
root_content: str,
|
||||
context: str,
|
||||
next_actor_id: int | None = None,
|
||||
next_probability: float = 0.3,
|
||||
next_delay_min: int = 30,
|
||||
next_delay_max: int = 7200,
|
||||
):
|
||||
parent_comment_id = str(parent_comment.get("id") or parent_comment.get("commentId") or "")
|
||||
db.add(PendingReplyTask(
|
||||
actor_user_id=actor_id,
|
||||
next_actor_user_id=next_actor_id,
|
||||
news_id=news_id,
|
||||
news_title=news_title,
|
||||
article_org_id=article_org_id,
|
||||
parent_comment_id=parent_comment_id,
|
||||
parent_comment=parent_comment,
|
||||
reply_to=reply_to,
|
||||
root_content=root_content,
|
||||
context=context,
|
||||
next_probability=next_probability,
|
||||
next_delay_min_seconds=next_delay_min,
|
||||
next_delay_max_seconds=next_delay_max,
|
||||
status=0,
|
||||
attempts=0,
|
||||
scheduled_at=datetime.now() + timedelta(seconds=max(0, delay_seconds)),
|
||||
))
|
||||
|
||||
async def _process_pending_reply_tasks(self):
|
||||
from app.services.news_service import news_service
|
||||
from app.services.ai_service import ai_service
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
try:
|
||||
now = datetime.now()
|
||||
await db.execute(
|
||||
update(PendingReplyTask)
|
||||
.where(
|
||||
PendingReplyTask.status == 1,
|
||||
PendingReplyTask.locked_at < now - timedelta(minutes=10),
|
||||
)
|
||||
.values(status=0, last_error="发送中超时,重新入队")
|
||||
)
|
||||
result = await db.execute(
|
||||
select(PendingReplyTask)
|
||||
.where(
|
||||
PendingReplyTask.status == 0,
|
||||
PendingReplyTask.scheduled_at <= now,
|
||||
)
|
||||
.order_by(PendingReplyTask.scheduled_at.asc())
|
||||
.limit(10)
|
||||
)
|
||||
tasks = result.scalars().all()
|
||||
for task in tasks:
|
||||
await self._process_pending_reply_task(db, task, news_service, ai_service)
|
||||
await db.commit()
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"待发送回复队列处理异常: {e}")
|
||||
|
||||
async def _process_pending_reply_task(self, db, task: PendingReplyTask, news_service, ai_service):
|
||||
task.status = 1
|
||||
task.locked_at = datetime.now()
|
||||
task.attempts = (task.attempts or 0) + 1
|
||||
await db.flush()
|
||||
|
||||
actor = await self._get_user_by_id(db, task.actor_user_id)
|
||||
if not actor or actor.status != 2 or actor.is_enabled != 1:
|
||||
task.status = 3
|
||||
task.last_error = "用户未登录或已禁用"
|
||||
return
|
||||
|
||||
reply_result = await self._post_contextual_reply(
|
||||
db=db,
|
||||
actor=actor,
|
||||
news_service=news_service,
|
||||
ai_service=ai_service,
|
||||
news_id=task.news_id,
|
||||
news_title=task.news_title or "",
|
||||
article_org_id=task.article_org_id or "",
|
||||
parent_comment=task.parent_comment or {},
|
||||
reply_to=task.reply_to or {},
|
||||
root_content=task.root_content or "",
|
||||
context=task.context or "",
|
||||
)
|
||||
if not reply_result["ok"]:
|
||||
task.status = 3 if task.attempts >= 3 else 0
|
||||
task.last_error = reply_result["error"] or "生成或发送回复失败"
|
||||
if task.status == 0:
|
||||
task.scheduled_at = datetime.now() + timedelta(minutes=5)
|
||||
logger.warning(f"待发送回复失败 task_id={task.id} user={actor.account}: {task.last_error}")
|
||||
return
|
||||
|
||||
task.status = 2
|
||||
task.sent_at = datetime.now()
|
||||
task.last_error = None
|
||||
await self._incr_total(db, actor.id)
|
||||
logger.info(f"💬 {actor.account} 发送了待发送回复 task_id={task.id}")
|
||||
|
||||
if task.next_actor_user_id and random.random() < float(task.next_probability or 0.3):
|
||||
delay_min = int(task.next_delay_min_seconds or 30)
|
||||
delay_max = max(delay_min, int(task.next_delay_max_seconds or 7200))
|
||||
next_delay = random.randint(delay_min, delay_max)
|
||||
await self._enqueue_pending_reply_task(
|
||||
db=db,
|
||||
delay_seconds=next_delay,
|
||||
actor_id=task.next_actor_user_id,
|
||||
news_id=task.news_id,
|
||||
news_title=task.news_title or "",
|
||||
article_org_id=task.article_org_id or "",
|
||||
parent_comment=task.parent_comment or {},
|
||||
reply_to=reply_result["reply"],
|
||||
root_content=task.root_content or "",
|
||||
context=(
|
||||
f"原评论:{task.root_content or ''}\n"
|
||||
f"上一条回复:{(task.reply_to or {}).get('content') or ''}\n"
|
||||
f"对方回应:{reply_result['content']}"
|
||||
),
|
||||
next_actor_id=None,
|
||||
next_probability=float(task.next_probability or 0.3),
|
||||
next_delay_min=delay_min,
|
||||
next_delay_max=delay_max,
|
||||
)
|
||||
logger.info(f"⏳ 已入队继续回复,延迟 {next_delay}s 后发送")
|
||||
|
||||
async def _post_contextual_reply(
|
||||
self,
|
||||
db,
|
||||
actor: VirtualUser,
|
||||
news_service,
|
||||
ai_service,
|
||||
news_id: str,
|
||||
news_title: str,
|
||||
article_org_id: str,
|
||||
parent_comment: dict,
|
||||
reply_to: dict,
|
||||
root_content: str,
|
||||
context: str,
|
||||
) -> dict:
|
||||
personality = await self._get_user_personality(db, actor.id)
|
||||
style_prompt = personality.comment_style_prompt if personality else ""
|
||||
word_min = personality.word_count_min if personality else 15
|
||||
word_max = min(personality.word_count_max, 80) if personality else 60
|
||||
content, tokens = await ai_service.generate_thread_reply(
|
||||
db, news_title, root_content, context,
|
||||
style_prompt, word_min, word_max,
|
||||
)
|
||||
if not content:
|
||||
return {"ok": False, "error": "", "reply": {}, "content": ""}
|
||||
|
||||
parent_comment_id = str(parent_comment.get("id") or parent_comment.get("commentId") or "")
|
||||
ok, err, reply_id = await news_service.post_reply_with_record_id(
|
||||
db, actor, news_id, parent_comment_id, content,
|
||||
parent_comment=parent_comment,
|
||||
reply_to=reply_to,
|
||||
article_title=news_title,
|
||||
org_id=article_org_id,
|
||||
)
|
||||
await self._save_record(
|
||||
db, actor, news_id, news_title, "reply",
|
||||
content, tokens, ok, err,
|
||||
parent_comment_id=parent_comment_id,
|
||||
platform_record_id=reply_id,
|
||||
)
|
||||
from app.core.redis_client import get_session
|
||||
sess = await get_session(actor.id)
|
||||
actor_uid = str(sess.get("platform_uid") or "") if sess else ""
|
||||
return {
|
||||
"ok": ok,
|
||||
"error": "" if ok else err,
|
||||
"content": content,
|
||||
"reply": {
|
||||
"id": reply_id,
|
||||
"content": content,
|
||||
"createUser": actor_uid,
|
||||
"fromUserName": actor.real_name or actor.nickname or actor.account,
|
||||
},
|
||||
}
|
||||
|
||||
async def _get_logged_in_user_by_platform_uid(self, db, platform_uid: str) -> VirtualUser | None:
|
||||
if not platform_uid:
|
||||
return None
|
||||
result = await db.execute(
|
||||
select(VirtualUser).where(
|
||||
VirtualUser.platform_uid == platform_uid,
|
||||
VirtualUser.status == 2,
|
||||
VirtualUser.is_enabled == 1,
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def _get_user_by_id(self, db, user_id: int) -> VirtualUser | None:
|
||||
result = await db.execute(select(VirtualUser).where(VirtualUser.id == user_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def _get_user_personality(self, db, user_id: int):
|
||||
result = await db.execute(
|
||||
select(UserPersonality).where(UserPersonality.user_id == user_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def _get_float_config(self, db, key: str, default: float) -> float:
|
||||
try:
|
||||
return float(await self._get_config_from_db(db, key, str(default)))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
async def _get_int_config(self, db, key: str, default: int) -> int:
|
||||
try:
|
||||
return int(float(await self._get_config_from_db(db, key, str(default))))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
async def _incr_total(self, db, user_id: int):
|
||||
await db.execute(
|
||||
@@ -511,4 +919,4 @@ class SchedulerService:
|
||||
logger.info("每日计数重置完成")
|
||||
|
||||
|
||||
scheduler_service = SchedulerService()
|
||||
scheduler_service = SchedulerService()
|
||||
|
||||
@@ -230,7 +230,7 @@ class StatsService:
|
||||
"comment": "评论", "reply": "回复", "like": "点赞",
|
||||
"collect": "收藏", "forward": "转发"
|
||||
}
|
||||
STATUS_LABELS = {0: "执行中", 1: "成功", 2: "失败"}
|
||||
STATUS_LABELS = {0: "执行中", 1: "成功", 2: "失败", 3: "已取消"}
|
||||
|
||||
items = []
|
||||
for r in records:
|
||||
|
||||
Reference in New Issue
Block a user