no message

This commit is contained in:
hajimi
2026-06-11 12:15:29 +08:00
parent 10ebe39c30
commit 96efa1d905
5859 changed files with 815501 additions and 5 deletions
@@ -0,0 +1,302 @@
<?php
namespace app\common\service\ai;
use app\common\model\ai\AiConfig;
use app\common\model\match\MatchEvent;
use think\facade\Cache;
class AiAssistantContextService
{
private const CRYPTO_SYMBOLS = [
'BTC' => ['BTC', '比特币', 'Bitcoin'],
'ETH' => ['ETH', '以太坊', 'Ethereum'],
'BNB' => ['BNB'],
'SOL' => ['SOL', 'Solana'],
'XRP' => ['XRP', 'Ripple'],
'DOGE' => ['DOGE', '狗狗币', 'Dogecoin'],
'TON' => ['TON', 'Toncoin'],
'TRX' => ['TRX', 'TRON', '波场'],
'ADA' => ['ADA', 'Cardano'],
'AVAX' => ['AVAX', 'Avalanche'],
];
public static function build(string $message, int $userId, int $sessionId): array
{
$topK = max(1, min(12, (int) AiConfig::getVal('assistant_kb_topk', '6')));
$hits = self::retrieveKnowledge($message, $userId, $sessionId, $topK);
$matchItems = self::searchMatches($message);
$cryptoItems = self::resolveCryptoContext($message);
$sources = [];
foreach ($hits as $hit) {
$sources[] = self::sourceFromKbHit($hit);
}
foreach ($matchItems as $item) {
$sources[] = $item['source'];
}
foreach ($cryptoItems as $item) {
$sources[] = $item['source'];
}
$sources = self::uniqueSources($sources);
$context = [
'knowledge_hits' => array_map([self::class, 'compactHit'], $hits),
'match_context' => array_column($matchItems, 'context'),
'crypto_realtime' => array_column($cryptoItems, 'context'),
];
return [
'context_text' => json_encode($context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
'sources' => array_values($sources),
'has_context' => !empty($hits) || !empty($matchItems) || !empty($cryptoItems),
];
}
private static function retrieveKnowledge(string $message, int $userId, int $sessionId, int $topK): array
{
$result = KbService::retrieve(
'assistant_chat',
$message,
[
'user_id' => $userId,
'ref_id' => $sessionId,
'domains' => ['article', 'post', 'lottery', 'match'],
],
['title' => $message],
$topK
);
if (empty($result['success']) || empty($result['hits']) || !is_array($result['hits'])) {
return [];
}
return $result['hits'];
}
private static function searchMatches(string $message): array
{
$terms = self::queryTerms($message);
if (empty($terms)) {
return [];
}
try {
$query = MatchEvent::where('is_show', 1);
$query->where(function ($q) use ($terms) {
foreach (array_slice($terms, 0, 5) as $term) {
$like = '%' . $term . '%';
$q->whereOr('home_team', 'like', $like)
->whereOr('away_team', 'like', $like)
->whereOr('league_name', 'like', $like);
}
});
$rows = $query->field('id,league_name,home_team,away_team,match_time,status,home_score,away_score,home_odds,draw_odds,away_odds')
->order('match_time desc')
->limit(3)
->select()
->toArray();
} catch (\Throwable $e) {
return [];
}
return array_map(static function (array $row) {
$title = trim(($row['home_team'] ?? '') . ' vs ' . ($row['away_team'] ?? ''));
$matchTime = !empty($row['match_time']) && is_numeric($row['match_time'])
? date('Y-m-d H:i', (int) $row['match_time'])
: (string) ($row['match_time'] ?? '');
$summary = sprintf(
'%s%s,比分 %s-%s,赔率 主胜%s 平%s 客胜%s。',
$row['league_name'] ?? '',
$matchTime,
$row['home_score'] ?? 0,
$row['away_score'] ?? 0,
$row['home_odds'] ?? '-',
$row['draw_odds'] ?? '-',
$row['away_odds'] ?? '-'
);
return [
'context' => [
'domain' => 'match',
'title' => $title,
'summary' => $summary,
],
'source' => [
'type' => 'match',
'title' => $title,
'summary' => $summary,
'path' => '/pages/match_detail/match_detail?id=' . (int) $row['id'],
'source_id' => (int) $row['id'],
],
];
}, $rows);
}
private static function resolveCryptoContext(string $message): array
{
if ((int) AiConfig::getVal('assistant_enable_crypto_realtime', '1') !== 1) {
return [];
}
$symbols = self::detectCryptoSymbols($message);
if (empty($symbols)) {
return [];
}
$items = [];
foreach (array_slice($symbols, 0, 3) as $symbol) {
$ticker = self::fetchBinanceTicker($symbol);
if (empty($ticker)) {
continue;
}
$price = (float) ($ticker['lastPrice'] ?? 0);
$change = (float) ($ticker['priceChangePercent'] ?? 0);
$high = (float) ($ticker['highPrice'] ?? 0);
$low = (float) ($ticker['lowPrice'] ?? 0);
$summary = sprintf(
'%s/USDT 当前价格 %.8g24小时涨跌 %.2f%%24小时高低 %.8g / %.8g。',
$symbol,
$price,
$change,
$high,
$low
);
$items[] = [
'context' => [
'domain' => 'crypto',
'symbol' => $symbol,
'summary' => $summary,
'raw' => [
'price' => $price,
'change_percent' => $change,
'high' => $high,
'low' => $low,
],
],
'source' => [
'type' => 'crypto',
'title' => $symbol . '/USDT 行情',
'summary' => $summary,
'path' => '/pages/crypto/crypto_detail?symbol=' . $symbol,
'source_id' => 0,
],
];
}
return $items;
}
private static function detectCryptoSymbols(string $message): array
{
$haystack = mb_strtolower($message);
$symbols = [];
foreach (self::CRYPTO_SYMBOLS as $symbol => $aliases) {
foreach ($aliases as $alias) {
if (mb_strpos($haystack, mb_strtolower($alias)) !== false) {
$symbols[] = $symbol;
break;
}
}
}
return array_values(array_unique($symbols));
}
private static function fetchBinanceTicker(string $symbol): array
{
$cacheKey = 'assistant_crypto_ticker_' . $symbol;
$cached = Cache::get($cacheKey);
if (is_array($cached)) {
return $cached;
}
$url = 'https://api.binance.com/api/v3/ticker/24hr?symbol=' . rawurlencode($symbol . 'USDT');
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 8,
CURLOPT_CONNECTTIMEOUT => 4,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);
$body = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = $httpCode === 200 ? json_decode((string) $body, true) : [];
if (is_array($data) && !empty($data['symbol'])) {
Cache::set($cacheKey, $data, 10);
return $data;
}
return [];
}
private static function sourceFromKbHit(array $hit): array
{
$domain = (string) ($hit['domain'] ?? '');
$sourceId = (int) ($hit['source_id'] ?? 0);
return [
'type' => $domain,
'title' => (string) ($hit['title'] ?? ''),
'summary' => self::clipText((string) ($hit['summary'] ?? $hit['chunk_text'] ?? ''), 120),
'path' => self::sourcePath($domain, $sourceId),
'source_id' => $sourceId,
];
}
private static function sourcePath(string $domain, int $sourceId): string
{
return match ($domain) {
'article' => '/pages/news_detail/news_detail?id=' . $sourceId,
'post' => '/packages_community/pages/post_detail?id=' . $sourceId,
'match' => '/pages/match_detail/match_detail?id=' . $sourceId,
'lottery' => '/pages/lottery_analysis/lottery_analysis',
default => '',
};
}
private static function compactHit(array $hit): array
{
return [
'domain' => $hit['domain'] ?? '',
'subtype' => $hit['subtype'] ?? '',
'source_id' => (int) ($hit['source_id'] ?? 0),
'title' => $hit['title'] ?? '',
'summary' => $hit['summary'] ?? '',
'content' => self::clipText((string) ($hit['chunk_text'] ?? ''), 360),
'score' => $hit['score'] ?? 0,
];
}
private static function uniqueSources(array $sources): array
{
$map = [];
foreach ($sources as $source) {
$key = ($source['type'] ?? '') . ':' . ($source['source_id'] ?? 0) . ':' . ($source['title'] ?? '');
if (!isset($map[$key]) && trim((string) ($source['title'] ?? '')) !== '') {
$map[$key] = $source;
}
}
return array_slice(array_values($map), 0, 8);
}
private static function queryTerms(string $message): array
{
$terms = preg_split('/[\s,,。!?;、:\/\\\\]+/u', trim($message)) ?: [];
$terms = array_values(array_filter(array_map('trim', $terms), static function (string $term) {
return mb_strlen($term) >= 2 && !in_array($term, ['今天', '最近', '分析', '什么', '一下'], true);
}));
return array_unique($terms);
}
private static function clipText(string $text, int $length): string
{
$text = trim(preg_replace('/\s+/u', ' ', $text) ?: '');
if (mb_strlen($text) <= $length) {
return $text;
}
return mb_substr($text, 0, $length) . '...';
}
}