From c3385b0e0434654282e3a5ee406bb1242bc1b9e2 Mon Sep 17 00:00:00 2001 From: hajimi Date: Sat, 13 Jun 2026 00:17:26 +0800 Subject: [PATCH] deploy: auto commit repo-root changes 2026-06-13 00:17:26 --- docker/crawler/ai_comment_dispatch.py | 47 +++- .../crawler/tests/test_ai_comment_dispatch.py | 8 + .../lists/community/CommentAccountLists.php | 23 +- .../article/ArticleAiCommentService.php | 79 +++++- .../src/packages_lottery/pages/ai_predict.vue | 243 +----------------- uniapp/src/pages/ai_analysis/ai_analysis.vue | 18 +- 6 files changed, 160 insertions(+), 258 deletions(-) diff --git a/docker/crawler/ai_comment_dispatch.py b/docker/crawler/ai_comment_dispatch.py index 56ffb9d..2301bd2 100644 --- a/docker/crawler/ai_comment_dispatch.py +++ b/docker/crawler/ai_comment_dispatch.py @@ -25,7 +25,40 @@ DEFAULT_REQUEST_TIMEOUT = 180 DEFAULT_RETRY_TIMES = 2 DEFAULT_RETRY_SLEEP_MS = 800 BOT_ACCOUNT_PREFIX = "ai_comment_" -BOT_NICKNAME_PREFIX = "AI评论员" +BOT_HANDLE_PREFIXES = [ + "看台", + "深夜", + "边线", + "北区", + "南看台", + "东门", + "西街", + "慢热", + "小酒馆", + "补时", + "角旗", + "传中", + "临场", + "半场", + "球门后", + "旧球鞋", + "赛点", + "风向", + "追球", + "回防", +] +BOT_HANDLE_SUFFIXES = [ + "阿泽", + "小顾", + "老韩", + "阿凯", + "小唐", + "阿林", + "阿松", + "阿北", + "阿岳", + "阿诚", +] DEFAULT_ARTICLE_PROMPT = ( "你是一名专业、克制、真实的体育资讯评论员。请根据给定资讯内容生成1条中文评论," @@ -592,7 +625,7 @@ class AiCommentRepository: for index in range(1, target_count + 1): persona = self._persona_for_index(index) account = f"{BOT_ACCOUNT_PREFIX}{index:04d}" - nickname = self._build_bot_nickname(index, persona["label"]) + nickname = self._build_bot_nickname(index) row = indexed.get(index) if row: if row["nickname"] != nickname: @@ -618,7 +651,7 @@ class AiCommentRepository: for row in rows: index = self._parse_account_index(row["account"]) persona = self._persona_for_index(index) - users.append(VirtualUser(int(row["id"]), row["account"], self._build_bot_nickname(index, persona["label"]), persona["key"])) + users.append(VirtualUser(int(row["id"]), row["account"], self._build_bot_nickname(index), persona["key"])) self._virtual_users_cache = list(users) return users[:target_count] @@ -991,9 +1024,11 @@ class AiCommentRepository: return hashlib.md5(str(random.random()).encode("utf-8")).hexdigest()[:12] @staticmethod - def _build_bot_nickname(index: int, persona_label: str) -> str: - number = f"{index:02d}" if index < 100 else str(index) - return f"{BOT_NICKNAME_PREFIX}{number}·{persona_label}" + def _build_bot_nickname(index: int) -> str: + zero_based = max(0, index - 1) + prefix = BOT_HANDLE_PREFIXES[zero_based % len(BOT_HANDLE_PREFIXES)] + suffix = BOT_HANDLE_SUFFIXES[(zero_based // len(BOT_HANDLE_PREFIXES)) % len(BOT_HANDLE_SUFFIXES)] + return f"{prefix}{suffix}" @staticmethod def _persona_for_index(index: int) -> Dict[str, str]: diff --git a/docker/crawler/tests/test_ai_comment_dispatch.py b/docker/crawler/tests/test_ai_comment_dispatch.py index 0314929..33e19fc 100644 --- a/docker/crawler/tests/test_ai_comment_dispatch.py +++ b/docker/crawler/tests/test_ai_comment_dispatch.py @@ -10,6 +10,7 @@ if str(ROOT) not in sys.path: from ai_comment_dispatch import ( AiCommentDispatcher, AiCommentSettings, + AiCommentRepository, CommentCandidate, DispatchTask, VirtualUser, @@ -209,6 +210,13 @@ class AiCommentDispatchTest(unittest.TestCase): self.assertEqual(settings.max_tokens, 300) self.assertAlmostEqual(settings.temperature, 0.65) + def test_generated_bot_nickname_uses_normal_handle(self): + nickname = AiCommentRepository._build_bot_nickname(17) + + self.assertNotIn("AI评论员", nickname) + self.assertNotIn("·", nickname) + self.assertGreaterEqual(len(nickname), 3) + if __name__ == "__main__": unittest.main() diff --git a/server/app/adminapi/lists/community/CommentAccountLists.php b/server/app/adminapi/lists/community/CommentAccountLists.php index 1e01848..85afad0 100644 --- a/server/app/adminapi/lists/community/CommentAccountLists.php +++ b/server/app/adminapi/lists/community/CommentAccountLists.php @@ -9,6 +9,21 @@ use think\facade\Db; class CommentAccountLists extends BaseAdminDataLists implements ListsSearchInterface { + protected const PERSONA_LABELS = [ + '数据派', + '老球迷', + '教练视角', + '夜班编辑', + '伤停控', + '赔率派', + '更衣室派', + '球探派', + '解说腔', + '主队党', + '模型党', + '资讯控', + ]; + public function setSearch(): array { return []; @@ -61,7 +76,7 @@ class CommentAccountLists extends BaseAdminDataLists implements ListsSearchInter } foreach ($lists as &$item) { - $item['persona_label'] = $this->resolvePersonaLabel((string) $item['nickname']); + $item['persona_label'] = $this->resolvePersonaLabel((string) $item['account'], (string) $item['nickname']); $item['article_comment_count'] = $articleCounts[(int) $item['id']] ?? 0; $item['community_comment_count'] = $communityCounts[(int) $item['id']] ?? 0; $item['total_comment_count'] = $item['article_comment_count'] + $item['community_comment_count']; @@ -85,12 +100,16 @@ class CommentAccountLists extends BaseAdminDataLists implements ListsSearchInter return $query->count(); } - protected function resolvePersonaLabel(string $nickname): string + protected function resolvePersonaLabel(string $account, string $nickname): string { if (str_contains($nickname, '·')) { $parts = explode('·', $nickname, 2); return $parts[1] ?: '-'; } + if (preg_match('/(\d+)$/', $account, $matches)) { + $index = max(1, (int) $matches[1]); + return self::PERSONA_LABELS[($index - 1) % count(self::PERSONA_LABELS)] ?? '-'; + } return '-'; } } diff --git a/server/app/common/service/article/ArticleAiCommentService.php b/server/app/common/service/article/ArticleAiCommentService.php index 316f04c..985646e 100644 --- a/server/app/common/service/article/ArticleAiCommentService.php +++ b/server/app/common/service/article/ArticleAiCommentService.php @@ -13,8 +13,41 @@ use think\facade\Db; class ArticleAiCommentService { private const VIRTUAL_ACCOUNT_PREFIX = 'ai_comment_'; - private const VIRTUAL_NICKNAME_PREFIX = 'AI评论员'; private const VIRTUAL_POOL_SIZE = 20; + private const VIRTUAL_NICKNAME_PREFIXES = [ + '看台', + '深夜', + '边线', + '北区', + '南看台', + '东门', + '西街', + '慢热', + '小酒馆', + '补时', + '角旗', + '传中', + '临场', + '半场', + '球门后', + '旧球鞋', + '赛点', + '风向', + '追球', + '回防', + ]; + private const VIRTUAL_NICKNAME_SUFFIXES = [ + '阿泽', + '小顾', + '老韩', + '阿凯', + '小唐', + '阿林', + '阿松', + '阿北', + '阿岳', + '阿诚', + ]; public static function tick(int $seedLimit = 10, int $processLimit = 20): array { @@ -158,12 +191,32 @@ class ArticleAiCommentService protected static function ensureVirtualUsers(int $targetCount = self::VIRTUAL_POOL_SIZE): array { - $existing = User::where('account', 'like', self::VIRTUAL_ACCOUNT_PREFIX . '%') + $existingRows = User::where('account', 'like', self::VIRTUAL_ACCOUNT_PREFIX . '%') ->order('id', 'asc') - ->column('id'); + ->field('id,account,nickname') + ->select() + ->toArray(); - $existing = array_values(array_map('intval', $existing)); + $existing = []; + foreach ($existingRows as $row) { + $existing[] = (int) $row['id']; + } $missing = max(0, $targetCount - count($existing)); + $now = time(); + foreach ($existingRows as $row) { + $index = self::parseAccountIndex((string) ($row['account'] ?? '')); + if ($index <= 0) { + continue; + } + $expectedNickname = self::buildVirtualNickname($index); + if (($row['nickname'] ?? '') !== $expectedNickname) { + User::update([ + 'id' => (int) $row['id'], + 'nickname' => $expectedNickname, + 'update_time' => $now, + ]); + } + } if ($missing > 0) { $avatar = (string) Config::get('project.default_image.user_avatar', ''); $passwordSalt = (string) Config::get('project.unique_identification', 'sport-era'); @@ -177,7 +230,7 @@ class ArticleAiCommentService 'sn' => $sn, 'avatar' => $avatar, 'real_name' => '', - 'nickname' => self::VIRTUAL_NICKNAME_PREFIX . str_pad((string) $accountIndex, 2, '0', STR_PAD_LEFT), + 'nickname' => self::buildVirtualNickname($accountIndex), 'account' => self::VIRTUAL_ACCOUNT_PREFIX . str_pad((string) $accountIndex, 4, '0', STR_PAD_LEFT), 'password' => $password, 'mobile' => '', @@ -196,6 +249,22 @@ class ArticleAiCommentService return $existing; } + protected static function buildVirtualNickname(int $index): string + { + $zeroBased = max(0, $index - 1); + $prefix = self::VIRTUAL_NICKNAME_PREFIXES[$zeroBased % count(self::VIRTUAL_NICKNAME_PREFIXES)]; + $suffix = self::VIRTUAL_NICKNAME_SUFFIXES[intdiv($zeroBased, count(self::VIRTUAL_NICKNAME_PREFIXES)) % count(self::VIRTUAL_NICKNAME_SUFFIXES)]; + return $prefix . $suffix; + } + + protected static function parseAccountIndex(string $account): int + { + if (!preg_match('/(\d+)$/', $account, $matches)) { + return 0; + } + return (int) $matches[1]; + } + protected static function resolveTaskCount(array $article): int { $commentCount = (int) ($article['comment_count'] ?? 0); diff --git a/uniapp/src/packages_lottery/pages/ai_predict.vue b/uniapp/src/packages_lottery/pages/ai_predict.vue index f1d8fc5..491eee4 100644 --- a/uniapp/src/packages_lottery/pages/ai_predict.vue +++ b/uniapp/src/packages_lottery/pages/ai_predict.vue @@ -166,102 +166,6 @@ - - - - 🎨 - 波色 & 生肖分析 - - - - - - - - - - - 🔴🔵🟢 波色统计 - - - 红波 - {{ - modules.colorZodiac.data.color_analysis.red.count }}次 - {{ - modules.colorZodiac.data.color_analysis.red.percentage }} - - - 蓝波 - {{ - modules.colorZodiac.data.color_analysis.blue.count }}次 - {{ - modules.colorZodiac.data.color_analysis.blue.percentage }} - - - 绿波 - {{ - modules.colorZodiac.data.color_analysis.green.count }}次 - {{ - modules.colorZodiac.data.color_analysis.green.percentage }} - - - - {{ colorZodiacTyped.colorText }} - | - - - - - 🐲 生肖分析 - - 热门生肖 - - {{ z }} - - - - 冷门生肖 - - {{ z }} - - - - {{ colorZodiacTyped.zodiacText }} - | - - - - - 💡 推荐 - - 推荐波色 - - {{ c }} - - - - 推荐生肖 - - {{ z }} - - - - {{ colorZodiacTyped.reasonText }} - | - - - - - — 分析完毕 — @@ -280,8 +184,7 @@ import { getLotteryAiHistory, getLotteryAiHotCold, getLotteryAiTrend, - getLotteryAiStats, - getLotteryAiColorZodiac + getLotteryAiStats } from '@/api/lottery' const statusBarHeight = ref(0) @@ -299,19 +202,15 @@ uni.getSystemInfo({ const goBack = () => uni.navigateBack() -const isHK6 = ref(false) - const modules = reactive({ history: { visible: false, loading: false, done: false, data: null as any }, hotCold: { visible: false, loading: false, done: false, data: null as any }, trend: { visible: false, loading: false, done: false, data: null as any }, stats: { visible: false, loading: false, done: false, data: null as any }, - colorZodiac: { visible: false, loading: false, done: false, data: null as any }, }) const allDone = computed(() => modules.history.done && modules.hotCold.done && modules.trend.done && modules.stats.done - && (!isHK6.value || modules.colorZodiac.done) ) // 历史数据逐行显示 @@ -324,8 +223,6 @@ const trendTyped = reactive({ trendText: '' }) const trendTyping = reactive({ trend: false }) const statsTyped = reactive({ consecutiveText: '', spanText: '', summaryText: '' }) const statsTyping = reactive({ consecutive: false, span: false, summary: false }) -const colorZodiacTyped = reactive({ colorText: '', zodiacText: '', reasonText: '' }) -const colorZodiacTyping = reactive({ color: false, zodiac: false, reason: false }) function typeText(fullText: string, target: any, key: string, typingFlag: any, flagKey: string, speed = 20, step = 2): Promise { return new Promise((resolve) => { @@ -416,15 +313,6 @@ function restoreFromCache(cached: any) { modules.stats.visible = true modules.stats.done = true } - // 波色生肖 - if (cached.colorZodiac) { - modules.colorZodiac.data = cached.colorZodiac - colorZodiacTyped.colorText = cached.colorZodiac.color_analysis?.summary || '' - colorZodiacTyped.zodiacText = cached.colorZodiac.zodiac_analysis?.summary || '' - colorZodiacTyped.reasonText = cached.colorZodiac.recommended?.reason || '' - modules.colorZodiac.visible = true - modules.colorZodiac.done = true - } } async function showDrawsSequentially(draws: any[]) { @@ -458,14 +346,10 @@ async function startAnalysis() { if (cached && latestIssue && latestIssue === cachedIssue) { modules.history.loading = false - isHK6.value = historyData?.template === 'HK6' restoreFromCache(cached) return } - // 判断是否六合彩 - isHK6.value = historyData?.template === 'HK6' - // 无缓存或期号不匹配 → 正常流式请求 const cacheData: any = {} @@ -543,29 +427,6 @@ async function startAnalysis() { modules.stats.done = true } - // 模块5: 波色生肖(仅六合彩) - if (isHK6.value) { - await delay(300) - modules.colorZodiac.visible = true - modules.colorZodiac.loading = true - try { - const data = await getLotteryAiColorZodiac({ code: gameCode.value }) - modules.colorZodiac.data = data - modules.colorZodiac.loading = false - modules.colorZodiac.done = true - cacheData.colorZodiac = data - await nextTick() - await typeText(data.color_analysis?.summary || '', colorZodiacTyped, 'colorText', colorZodiacTyping, 'color') - await delay(100) - await typeText(data.zodiac_analysis?.summary || '', colorZodiacTyped, 'zodiacText', colorZodiacTyping, 'zodiac') - await delay(100) - await typeText(data.recommended?.reason || '', colorZodiacTyped, 'reasonText', colorZodiacTyping, 'reason') - } catch { - modules.colorZodiac.loading = false - modules.colorZodiac.done = true - } - } - // 全部完成后存入缓存 if (cacheData.history) { saveCache(gameCode.value, cacheData) @@ -912,108 +773,6 @@ onLoad((opts: any) => { } } -/* 波色分析 */ -.ai-color-block { - margin-bottom: 16rpx; -} - -.ai-color-grid { - display: flex; - gap: 12rpx; - margin-top: 12rpx; -} - -.ai-color-card { - flex: 1; - border-radius: 12rpx; - padding: 16rpx 12rpx; - text-align: center; - - &--red { - background: rgba(239, 68, 68, 0.1); - border: 1rpx solid rgba(239, 68, 68, 0.3); - } - - &--blue { - background: rgba(59, 130, 246, 0.1); - border: 1rpx solid rgba(59, 130, 246, 0.3); - } - - &--green { - background: rgba(34, 197, 94, 0.1); - border: 1rpx solid rgba(34, 197, 94, 0.3); - } - - &__name { - font-size: 24rpx; - font-weight: bold; - color: #333; - display: block; - } - - &__count { - font-size: 28rpx; - font-weight: bold; - color: #333; - margin-top: 4rpx; - display: block; - } - - &__pct { - font-size: 22rpx; - color: #999; - margin-top: 2rpx; - display: block; - } -} - -/* 生肖分析 */ -.ai-zodiac-block { - margin-bottom: 16rpx; -} - -.ai-zodiac-row { - margin-top: 12rpx; - - &__label { - font-size: 24rpx; - color: #666; - margin-bottom: 8rpx; - display: block; - } -} - -.ai-zodiac-tags { - display: flex; - flex-wrap: wrap; - gap: 10rpx; -} - -.ai-zodiac-tag { - font-size: 24rpx; - padding: 6rpx 16rpx; - border-radius: 8rpx; - font-weight: 500; - - &--hot { - background: rgba(239, 68, 68, 0.1); - color: #dc2626; - border: 1rpx solid rgba(239, 68, 68, 0.2); - } - - &--cold { - background: rgba(59, 130, 246, 0.1); - color: #2563eb; - border: 1rpx solid rgba(59, 130, 246, 0.2); - } - - &--rec { - background: linear-gradient(135deg, rgba(102, 126, 234, 0.15), rgba(118, 75, 162, 0.15)); - color: #667eea; - border: 1rpx solid rgba(102, 126, 234, 0.3); - } -} - /* 免责声明 */ .ai-disclaimer { background: #fffbeb; diff --git a/uniapp/src/pages/ai_analysis/ai_analysis.vue b/uniapp/src/pages/ai_analysis/ai_analysis.vue index b2e23c9..a9099d8 100644 --- a/uniapp/src/pages/ai_analysis/ai_analysis.vue +++ b/uniapp/src/pages/ai_analysis/ai_analysis.vue @@ -646,8 +646,16 @@ const getRawAnalysisErrorMessage = (error: any) => { : String(error?.msg || error?.errMsg || error?.message || '') } +const getAnalysisHttpStatus = (error: any) => { + return Number(error?.statusCode || error?.status || error?.response?.statusCode || error?.response?.status || 0) +} + const isAnalysisTransportError = (error: any) => { - return /timeout|request:fail|abort|网络|network/i.test(getRawAnalysisErrorMessage(error)) + const status = getAnalysisHttpStatus(error) + if ([502, 503, 504].includes(status)) { + return true + } + return /timeout|request:fail|abort|网络|network|Bad Gateway|Service Unavailable|Gateway Timeout|网关/i.test(getRawAnalysisErrorMessage(error)) } const extractPostAnalysisPacket = (res: any) => { @@ -667,7 +675,11 @@ const shouldRefreshPostAnalysisCache = (packet: { analysis: any; fromCache: bool const getAnalysisErrorMessage = (error: any) => { const rawMessage = getRawAnalysisErrorMessage(error) + const status = getAnalysisHttpStatus(error) + if ([502, 503, 504].includes(status) || /Bad Gateway|Service Unavailable|Gateway Timeout|网关/i.test(rawMessage)) { + return 'AI分析服务正在排队或网关暂时断开,请稍后重新进入分析页查看结果' + } if (/timeout|超时/i.test(rawMessage)) { return 'AI分析耗时较长,请稍后重新分析;页面已延长等待时间,但当前网络或模型响应仍超时' } @@ -888,7 +900,7 @@ const fetchPostAnalysis = async (postId: number) => { packet = extractPostAnalysisPacket(await getCommunityPostAiAnalysis({ post_id: postId, force: 1 })) } if (!packet.analysis) { - throw new Error('AI分析结果为空,请稍后重试') + throw { message: 'AI分析结果为空,请稍后重试', statusCode: 502 } } analysisData.value = packet.analysis } catch (e: any) { @@ -897,7 +909,7 @@ const fetchPostAnalysis = async (postId: number) => { try { const resolved = await waitForPostAnalysisResult(postId) if (!resolved) { - errorMsg.value = 'AI分析仍在生成或网络连接不稳定,请稍后重新进入分析页查看结果' + errorMsg.value = getAnalysisErrorMessage(e) } } catch (pollError: any) { errorMsg.value = getAnalysisErrorMessage(pollError)