deploy: auto commit repo-root changes 2026-06-13 00:17:26
This commit is contained in:
@@ -25,7 +25,40 @@ DEFAULT_REQUEST_TIMEOUT = 180
|
|||||||
DEFAULT_RETRY_TIMES = 2
|
DEFAULT_RETRY_TIMES = 2
|
||||||
DEFAULT_RETRY_SLEEP_MS = 800
|
DEFAULT_RETRY_SLEEP_MS = 800
|
||||||
BOT_ACCOUNT_PREFIX = "ai_comment_"
|
BOT_ACCOUNT_PREFIX = "ai_comment_"
|
||||||
BOT_NICKNAME_PREFIX = "AI评论员"
|
BOT_HANDLE_PREFIXES = [
|
||||||
|
"看台",
|
||||||
|
"深夜",
|
||||||
|
"边线",
|
||||||
|
"北区",
|
||||||
|
"南看台",
|
||||||
|
"东门",
|
||||||
|
"西街",
|
||||||
|
"慢热",
|
||||||
|
"小酒馆",
|
||||||
|
"补时",
|
||||||
|
"角旗",
|
||||||
|
"传中",
|
||||||
|
"临场",
|
||||||
|
"半场",
|
||||||
|
"球门后",
|
||||||
|
"旧球鞋",
|
||||||
|
"赛点",
|
||||||
|
"风向",
|
||||||
|
"追球",
|
||||||
|
"回防",
|
||||||
|
]
|
||||||
|
BOT_HANDLE_SUFFIXES = [
|
||||||
|
"阿泽",
|
||||||
|
"小顾",
|
||||||
|
"老韩",
|
||||||
|
"阿凯",
|
||||||
|
"小唐",
|
||||||
|
"阿林",
|
||||||
|
"阿松",
|
||||||
|
"阿北",
|
||||||
|
"阿岳",
|
||||||
|
"阿诚",
|
||||||
|
]
|
||||||
|
|
||||||
DEFAULT_ARTICLE_PROMPT = (
|
DEFAULT_ARTICLE_PROMPT = (
|
||||||
"你是一名专业、克制、真实的体育资讯评论员。请根据给定资讯内容生成1条中文评论,"
|
"你是一名专业、克制、真实的体育资讯评论员。请根据给定资讯内容生成1条中文评论,"
|
||||||
@@ -592,7 +625,7 @@ class AiCommentRepository:
|
|||||||
for index in range(1, target_count + 1):
|
for index in range(1, target_count + 1):
|
||||||
persona = self._persona_for_index(index)
|
persona = self._persona_for_index(index)
|
||||||
account = f"{BOT_ACCOUNT_PREFIX}{index:04d}"
|
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)
|
row = indexed.get(index)
|
||||||
if row:
|
if row:
|
||||||
if row["nickname"] != nickname:
|
if row["nickname"] != nickname:
|
||||||
@@ -618,7 +651,7 @@ class AiCommentRepository:
|
|||||||
for row in rows:
|
for row in rows:
|
||||||
index = self._parse_account_index(row["account"])
|
index = self._parse_account_index(row["account"])
|
||||||
persona = self._persona_for_index(index)
|
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)
|
self._virtual_users_cache = list(users)
|
||||||
return users[:target_count]
|
return users[:target_count]
|
||||||
|
|
||||||
@@ -991,9 +1024,11 @@ class AiCommentRepository:
|
|||||||
return hashlib.md5(str(random.random()).encode("utf-8")).hexdigest()[:12]
|
return hashlib.md5(str(random.random()).encode("utf-8")).hexdigest()[:12]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_bot_nickname(index: int, persona_label: str) -> str:
|
def _build_bot_nickname(index: int) -> str:
|
||||||
number = f"{index:02d}" if index < 100 else str(index)
|
zero_based = max(0, index - 1)
|
||||||
return f"{BOT_NICKNAME_PREFIX}{number}·{persona_label}"
|
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
|
@staticmethod
|
||||||
def _persona_for_index(index: int) -> Dict[str, str]:
|
def _persona_for_index(index: int) -> Dict[str, str]:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ if str(ROOT) not in sys.path:
|
|||||||
from ai_comment_dispatch import (
|
from ai_comment_dispatch import (
|
||||||
AiCommentDispatcher,
|
AiCommentDispatcher,
|
||||||
AiCommentSettings,
|
AiCommentSettings,
|
||||||
|
AiCommentRepository,
|
||||||
CommentCandidate,
|
CommentCandidate,
|
||||||
DispatchTask,
|
DispatchTask,
|
||||||
VirtualUser,
|
VirtualUser,
|
||||||
@@ -209,6 +210,13 @@ class AiCommentDispatchTest(unittest.TestCase):
|
|||||||
self.assertEqual(settings.max_tokens, 300)
|
self.assertEqual(settings.max_tokens, 300)
|
||||||
self.assertAlmostEqual(settings.temperature, 0.65)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -9,6 +9,21 @@ use think\facade\Db;
|
|||||||
|
|
||||||
class CommentAccountLists extends BaseAdminDataLists implements ListsSearchInterface
|
class CommentAccountLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||||
{
|
{
|
||||||
|
protected const PERSONA_LABELS = [
|
||||||
|
'数据派',
|
||||||
|
'老球迷',
|
||||||
|
'教练视角',
|
||||||
|
'夜班编辑',
|
||||||
|
'伤停控',
|
||||||
|
'赔率派',
|
||||||
|
'更衣室派',
|
||||||
|
'球探派',
|
||||||
|
'解说腔',
|
||||||
|
'主队党',
|
||||||
|
'模型党',
|
||||||
|
'资讯控',
|
||||||
|
];
|
||||||
|
|
||||||
public function setSearch(): array
|
public function setSearch(): array
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
@@ -61,7 +76,7 @@ class CommentAccountLists extends BaseAdminDataLists implements ListsSearchInter
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($lists as &$item) {
|
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['article_comment_count'] = $articleCounts[(int) $item['id']] ?? 0;
|
||||||
$item['community_comment_count'] = $communityCounts[(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'];
|
$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();
|
return $query->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function resolvePersonaLabel(string $nickname): string
|
protected function resolvePersonaLabel(string $account, string $nickname): string
|
||||||
{
|
{
|
||||||
if (str_contains($nickname, '·')) {
|
if (str_contains($nickname, '·')) {
|
||||||
$parts = explode('·', $nickname, 2);
|
$parts = explode('·', $nickname, 2);
|
||||||
return $parts[1] ?: '-';
|
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 '-';
|
return '-';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,41 @@ use think\facade\Db;
|
|||||||
class ArticleAiCommentService
|
class ArticleAiCommentService
|
||||||
{
|
{
|
||||||
private const VIRTUAL_ACCOUNT_PREFIX = 'ai_comment_';
|
private const VIRTUAL_ACCOUNT_PREFIX = 'ai_comment_';
|
||||||
private const VIRTUAL_NICKNAME_PREFIX = 'AI评论员';
|
|
||||||
private const VIRTUAL_POOL_SIZE = 20;
|
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
|
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
|
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')
|
->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));
|
$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) {
|
if ($missing > 0) {
|
||||||
$avatar = (string) Config::get('project.default_image.user_avatar', '');
|
$avatar = (string) Config::get('project.default_image.user_avatar', '');
|
||||||
$passwordSalt = (string) Config::get('project.unique_identification', 'sport-era');
|
$passwordSalt = (string) Config::get('project.unique_identification', 'sport-era');
|
||||||
@@ -177,7 +230,7 @@ class ArticleAiCommentService
|
|||||||
'sn' => $sn,
|
'sn' => $sn,
|
||||||
'avatar' => $avatar,
|
'avatar' => $avatar,
|
||||||
'real_name' => '',
|
'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),
|
'account' => self::VIRTUAL_ACCOUNT_PREFIX . str_pad((string) $accountIndex, 4, '0', STR_PAD_LEFT),
|
||||||
'password' => $password,
|
'password' => $password,
|
||||||
'mobile' => '',
|
'mobile' => '',
|
||||||
@@ -196,6 +249,22 @@ class ArticleAiCommentService
|
|||||||
return $existing;
|
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
|
protected static function resolveTaskCount(array $article): int
|
||||||
{
|
{
|
||||||
$commentCount = (int) ($article['comment_count'] ?? 0);
|
$commentCount = (int) ($article['comment_count'] ?? 0);
|
||||||
|
|||||||
@@ -166,102 +166,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 模块5: 波色+生肖分析(六合彩专属) -->
|
|
||||||
<view v-if="isHK6" class="ai-section" :class="{ 'ai-section--visible': modules.colorZodiac.visible }">
|
|
||||||
<view class="ai-section__header">
|
|
||||||
<view class="ai-section__icon ai-section__icon--color">🎨</view>
|
|
||||||
<text class="ai-section__title">波色 & 生肖分析</text>
|
|
||||||
<view v-if="modules.colorZodiac.loading" class="ai-section__loading">
|
|
||||||
<view class="ai-dot" />
|
|
||||||
<view class="ai-dot" />
|
|
||||||
<view class="ai-dot" />
|
|
||||||
</view>
|
|
||||||
<text v-else-if="modules.colorZodiac.done" class="ai-section__done">✓</text>
|
|
||||||
</view>
|
|
||||||
<view v-if="modules.colorZodiac.data" class="ai-section__body">
|
|
||||||
<!-- 波色分析 -->
|
|
||||||
<view v-if="modules.colorZodiac.data.color_analysis" class="ai-color-block">
|
|
||||||
<text class="ai-nums-block__label">🔴🔵🟢 波色统计</text>
|
|
||||||
<view class="ai-color-grid">
|
|
||||||
<view v-if="modules.colorZodiac.data.color_analysis.red"
|
|
||||||
class="ai-color-card ai-color-card--red">
|
|
||||||
<text class="ai-color-card__name">红波</text>
|
|
||||||
<text class="ai-color-card__count">{{
|
|
||||||
modules.colorZodiac.data.color_analysis.red.count }}次</text>
|
|
||||||
<text class="ai-color-card__pct">{{
|
|
||||||
modules.colorZodiac.data.color_analysis.red.percentage }}</text>
|
|
||||||
</view>
|
|
||||||
<view v-if="modules.colorZodiac.data.color_analysis.blue"
|
|
||||||
class="ai-color-card ai-color-card--blue">
|
|
||||||
<text class="ai-color-card__name">蓝波</text>
|
|
||||||
<text class="ai-color-card__count">{{
|
|
||||||
modules.colorZodiac.data.color_analysis.blue.count }}次</text>
|
|
||||||
<text class="ai-color-card__pct">{{
|
|
||||||
modules.colorZodiac.data.color_analysis.blue.percentage }}</text>
|
|
||||||
</view>
|
|
||||||
<view v-if="modules.colorZodiac.data.color_analysis.green"
|
|
||||||
class="ai-color-card ai-color-card--green">
|
|
||||||
<text class="ai-color-card__name">绿波</text>
|
|
||||||
<text class="ai-color-card__count">{{
|
|
||||||
modules.colorZodiac.data.color_analysis.green.count }}次</text>
|
|
||||||
<text class="ai-color-card__pct">{{
|
|
||||||
modules.colorZodiac.data.color_analysis.green.percentage }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view v-if="modules.colorZodiac.data.color_analysis.summary" class="ai-text-block">
|
|
||||||
<text class="ai-typewriter">{{ colorZodiacTyped.colorText }}</text>
|
|
||||||
<text v-if="colorZodiacTyping.color" class="ai-cursor">|</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<!-- 生肖分析 -->
|
|
||||||
<view v-if="modules.colorZodiac.data.zodiac_analysis" class="ai-zodiac-block">
|
|
||||||
<text class="ai-nums-block__label">🐲 生肖分析</text>
|
|
||||||
<view v-if="modules.colorZodiac.data.zodiac_analysis.hot_zodiac?.length"
|
|
||||||
class="ai-zodiac-row">
|
|
||||||
<text class="ai-zodiac-row__label">热门生肖</text>
|
|
||||||
<view class="ai-zodiac-tags">
|
|
||||||
<text v-for="z in modules.colorZodiac.data.zodiac_analysis.hot_zodiac"
|
|
||||||
:key="'hz' + z" class="ai-zodiac-tag ai-zodiac-tag--hot">{{ z }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view v-if="modules.colorZodiac.data.zodiac_analysis.cold_zodiac?.length"
|
|
||||||
class="ai-zodiac-row">
|
|
||||||
<text class="ai-zodiac-row__label">冷门生肖</text>
|
|
||||||
<view class="ai-zodiac-tags">
|
|
||||||
<text v-for="z in modules.colorZodiac.data.zodiac_analysis.cold_zodiac"
|
|
||||||
:key="'cz' + z" class="ai-zodiac-tag ai-zodiac-tag--cold">{{ z }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view v-if="modules.colorZodiac.data.zodiac_analysis.summary" class="ai-text-block">
|
|
||||||
<text class="ai-typewriter">{{ colorZodiacTyped.zodiacText }}</text>
|
|
||||||
<text v-if="colorZodiacTyping.zodiac" class="ai-cursor">|</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<!-- 推荐 -->
|
|
||||||
<view v-if="modules.colorZodiac.data.recommended" class="ai-text-block">
|
|
||||||
<text class="ai-nums-block__label">💡 推荐</text>
|
|
||||||
<view v-if="modules.colorZodiac.data.recommended.colors?.length" class="ai-zodiac-row">
|
|
||||||
<text class="ai-zodiac-row__label">推荐波色</text>
|
|
||||||
<view class="ai-zodiac-tags">
|
|
||||||
<text v-for="c in modules.colorZodiac.data.recommended.colors" :key="'rc' + c"
|
|
||||||
class="ai-zodiac-tag ai-zodiac-tag--rec">{{ c }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view v-if="modules.colorZodiac.data.recommended.zodiacs?.length" class="ai-zodiac-row">
|
|
||||||
<text class="ai-zodiac-row__label">推荐生肖</text>
|
|
||||||
<view class="ai-zodiac-tags">
|
|
||||||
<text v-for="z in modules.colorZodiac.data.recommended.zodiacs" :key="'rz' + z"
|
|
||||||
class="ai-zodiac-tag ai-zodiac-tag--rec">{{ z }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view v-if="modules.colorZodiac.data.recommended.reason" class="ai-text-block">
|
|
||||||
<text class="ai-typewriter">{{ colorZodiacTyped.reasonText }}</text>
|
|
||||||
<text v-if="colorZodiacTyping.reason" class="ai-cursor">|</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 全部完成 -->
|
<!-- 全部完成 -->
|
||||||
<view v-if="allDone" class="ai-footer">
|
<view v-if="allDone" class="ai-footer">
|
||||||
<text class="ai-footer__text">— 分析完毕 —</text>
|
<text class="ai-footer__text">— 分析完毕 —</text>
|
||||||
@@ -280,8 +184,7 @@ import {
|
|||||||
getLotteryAiHistory,
|
getLotteryAiHistory,
|
||||||
getLotteryAiHotCold,
|
getLotteryAiHotCold,
|
||||||
getLotteryAiTrend,
|
getLotteryAiTrend,
|
||||||
getLotteryAiStats,
|
getLotteryAiStats
|
||||||
getLotteryAiColorZodiac
|
|
||||||
} from '@/api/lottery'
|
} from '@/api/lottery'
|
||||||
|
|
||||||
const statusBarHeight = ref(0)
|
const statusBarHeight = ref(0)
|
||||||
@@ -299,19 +202,15 @@ uni.getSystemInfo({
|
|||||||
|
|
||||||
const goBack = () => uni.navigateBack()
|
const goBack = () => uni.navigateBack()
|
||||||
|
|
||||||
const isHK6 = ref(false)
|
|
||||||
|
|
||||||
const modules = reactive({
|
const modules = reactive({
|
||||||
history: { visible: false, loading: false, done: false, data: null as any },
|
history: { visible: false, loading: false, done: false, data: null as any },
|
||||||
hotCold: { 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 },
|
trend: { visible: false, loading: false, done: false, data: null as any },
|
||||||
stats: { 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(() =>
|
const allDone = computed(() =>
|
||||||
modules.history.done && modules.hotCold.done && modules.trend.done && modules.stats.done
|
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 trendTyping = reactive({ trend: false })
|
||||||
const statsTyped = reactive({ consecutiveText: '', spanText: '', summaryText: '' })
|
const statsTyped = reactive({ consecutiveText: '', spanText: '', summaryText: '' })
|
||||||
const statsTyping = reactive({ consecutive: false, span: false, summary: false })
|
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<void> {
|
function typeText(fullText: string, target: any, key: string, typingFlag: any, flagKey: string, speed = 20, step = 2): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -416,15 +313,6 @@ function restoreFromCache(cached: any) {
|
|||||||
modules.stats.visible = true
|
modules.stats.visible = true
|
||||||
modules.stats.done = 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[]) {
|
async function showDrawsSequentially(draws: any[]) {
|
||||||
@@ -458,14 +346,10 @@ async function startAnalysis() {
|
|||||||
|
|
||||||
if (cached && latestIssue && latestIssue === cachedIssue) {
|
if (cached && latestIssue && latestIssue === cachedIssue) {
|
||||||
modules.history.loading = false
|
modules.history.loading = false
|
||||||
isHK6.value = historyData?.template === 'HK6'
|
|
||||||
restoreFromCache(cached)
|
restoreFromCache(cached)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否六合彩
|
|
||||||
isHK6.value = historyData?.template === 'HK6'
|
|
||||||
|
|
||||||
// 无缓存或期号不匹配 → 正常流式请求
|
// 无缓存或期号不匹配 → 正常流式请求
|
||||||
const cacheData: any = {}
|
const cacheData: any = {}
|
||||||
|
|
||||||
@@ -543,29 +427,6 @@ async function startAnalysis() {
|
|||||||
modules.stats.done = true
|
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) {
|
if (cacheData.history) {
|
||||||
saveCache(gameCode.value, cacheData)
|
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 {
|
.ai-disclaimer {
|
||||||
background: #fffbeb;
|
background: #fffbeb;
|
||||||
|
|||||||
@@ -646,8 +646,16 @@ const getRawAnalysisErrorMessage = (error: any) => {
|
|||||||
: String(error?.msg || error?.errMsg || error?.message || '')
|
: 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) => {
|
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) => {
|
const extractPostAnalysisPacket = (res: any) => {
|
||||||
@@ -667,7 +675,11 @@ const shouldRefreshPostAnalysisCache = (packet: { analysis: any; fromCache: bool
|
|||||||
|
|
||||||
const getAnalysisErrorMessage = (error: any) => {
|
const getAnalysisErrorMessage = (error: any) => {
|
||||||
const rawMessage = getRawAnalysisErrorMessage(error)
|
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)) {
|
if (/timeout|超时/i.test(rawMessage)) {
|
||||||
return 'AI分析耗时较长,请稍后重新分析;页面已延长等待时间,但当前网络或模型响应仍超时'
|
return 'AI分析耗时较长,请稍后重新分析;页面已延长等待时间,但当前网络或模型响应仍超时'
|
||||||
}
|
}
|
||||||
@@ -888,7 +900,7 @@ const fetchPostAnalysis = async (postId: number) => {
|
|||||||
packet = extractPostAnalysisPacket(await getCommunityPostAiAnalysis({ post_id: postId, force: 1 }))
|
packet = extractPostAnalysisPacket(await getCommunityPostAiAnalysis({ post_id: postId, force: 1 }))
|
||||||
}
|
}
|
||||||
if (!packet.analysis) {
|
if (!packet.analysis) {
|
||||||
throw new Error('AI分析结果为空,请稍后重试')
|
throw { message: 'AI分析结果为空,请稍后重试', statusCode: 502 }
|
||||||
}
|
}
|
||||||
analysisData.value = packet.analysis
|
analysisData.value = packet.analysis
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -897,7 +909,7 @@ const fetchPostAnalysis = async (postId: number) => {
|
|||||||
try {
|
try {
|
||||||
const resolved = await waitForPostAnalysisResult(postId)
|
const resolved = await waitForPostAnalysisResult(postId)
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
errorMsg.value = 'AI分析仍在生成或网络连接不稳定,请稍后重新进入分析页查看结果'
|
errorMsg.value = getAnalysisErrorMessage(e)
|
||||||
}
|
}
|
||||||
} catch (pollError: any) {
|
} catch (pollError: any) {
|
||||||
errorMsg.value = getAnalysisErrorMessage(pollError)
|
errorMsg.value = getAnalysisErrorMessage(pollError)
|
||||||
|
|||||||
Reference in New Issue
Block a user