deploy: auto commit repo-root changes 2026-06-13 11:35:15
This commit is contained in:
@@ -15,6 +15,7 @@ use app\common\logic\AccountLogLogic;
|
||||
use app\common\service\ai\AiService;
|
||||
use app\common\service\ai\KbSyncService;
|
||||
use app\common\service\chat\PrivateChatService;
|
||||
use app\common\service\FileService;
|
||||
use app\common\service\VipService;
|
||||
use think\facade\Db;
|
||||
|
||||
@@ -703,9 +704,169 @@ class CommunityController extends BaseApiController
|
||||
return in_array($lotteryKey, ['a6', 'xa6'], true);
|
||||
}
|
||||
|
||||
private function localizeLiuhePostImagesForAi(CommunityPost $post, array $images): array
|
||||
{
|
||||
$localizedImages = [];
|
||||
$changed = false;
|
||||
|
||||
foreach ($images as $image) {
|
||||
$image = trim((string) $image);
|
||||
if ($image === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->isLocalUploadImage($image)) {
|
||||
$localUrl = $this->normalizeLocalUploadImageUrl($image);
|
||||
$localizedImages[] = $localUrl;
|
||||
$changed = $changed || $localUrl !== $image;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!preg_match('/^https?:\/\//i', $image)) {
|
||||
$localUrl = FileService::getFileUrl(ltrim($image, '/'));
|
||||
$localizedImages[] = $localUrl;
|
||||
$changed = $changed || $localUrl !== $image;
|
||||
continue;
|
||||
}
|
||||
|
||||
$localUrl = $this->saveRemoteImageForAiAnalysis($image);
|
||||
if ($localUrl === '') {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => '图片下载到本地失败,请稍后重试',
|
||||
'images' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$localizedImages[] = $localUrl;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
if (empty($localizedImages)) {
|
||||
return ['success' => false, 'error' => '帖子图片为空,无法分析', 'images' => []];
|
||||
}
|
||||
|
||||
if ($changed || $localizedImages !== array_values($images)) {
|
||||
$now = time();
|
||||
Db::name('community_post')->where('id', (int) $post->id)->update([
|
||||
'images' => json_encode($localizedImages, JSON_UNESCAPED_UNICODE),
|
||||
'update_time' => $now,
|
||||
]);
|
||||
|
||||
Db::name('community_post_image')->where('post_id', (int) $post->id)->delete();
|
||||
foreach ($localizedImages as $index => $imageUrl) {
|
||||
Db::name('community_post_image')->insert([
|
||||
'post_id' => (int) $post->id,
|
||||
'image_url' => $imageUrl,
|
||||
'sort' => $index,
|
||||
'create_time' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return ['success' => true, 'images' => $localizedImages, 'changed' => $changed];
|
||||
}
|
||||
|
||||
private function isLocalUploadImage(string $image): bool
|
||||
{
|
||||
if (!preg_match('/^https?:\/\//i', $image)) {
|
||||
return str_starts_with(ltrim($image, '/'), 'uploads/');
|
||||
}
|
||||
|
||||
$path = (string) (parse_url($image, PHP_URL_PATH) ?: '');
|
||||
if (!str_starts_with($path, '/uploads/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$imageHost = strtolower((string) (parse_url($image, PHP_URL_HOST) ?: ''));
|
||||
$currentHost = strtolower((string) (parse_url(request()->domain(), PHP_URL_HOST) ?: ''));
|
||||
return $imageHost !== '' && $currentHost !== '' && $imageHost === $currentHost;
|
||||
}
|
||||
|
||||
private function normalizeLocalUploadImageUrl(string $image): string
|
||||
{
|
||||
if (preg_match('/^https?:\/\//i', $image)) {
|
||||
return $image;
|
||||
}
|
||||
|
||||
return FileService::getFileUrl(ltrim($image, '/'));
|
||||
}
|
||||
|
||||
private function saveRemoteImageForAiAnalysis(string $imageUrl): string
|
||||
{
|
||||
$ch = curl_init($imageUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 10,
|
||||
CURLOPT_TIMEOUT => 25,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_USERAGENT => 'Mozilla/5.0 SportEraBot/1.0',
|
||||
]);
|
||||
|
||||
$body = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$contentType = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200 || !is_string($body) || $body === '' || strlen($body) > 8 * 1024 * 1024) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$mime = strtolower(trim(explode(';', $contentType)[0] ?? ''));
|
||||
$imageInfo = @getimagesizefromstring($body);
|
||||
if (is_array($imageInfo) && !empty($imageInfo['mime'])) {
|
||||
$mime = strtolower((string) $imageInfo['mime']);
|
||||
}
|
||||
|
||||
$extension = match ($mime) {
|
||||
'image/jpeg', 'image/jpg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
'image/webp' => 'webp',
|
||||
'image/gif' => 'gif',
|
||||
default => '',
|
||||
};
|
||||
|
||||
if ($extension === '') {
|
||||
$path = parse_url($imageUrl, PHP_URL_PATH) ?: '';
|
||||
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
if (!in_array($extension, ['jpg', 'jpeg', 'png', 'webp', 'gif'], true)) {
|
||||
return '';
|
||||
}
|
||||
$extension = $extension === 'jpeg' ? 'jpg' : $extension;
|
||||
}
|
||||
|
||||
$relativeDir = 'uploads/ai_lottery_posts/' . date('Ymd');
|
||||
$publicRoot = rtrim(public_path(), DIRECTORY_SEPARATOR . '/');
|
||||
$targetDir = $publicRoot . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relativeDir);
|
||||
if (!is_dir($targetDir) && !mkdir($targetDir, 0755, true) && !is_dir($targetDir)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$filename = sha1($imageUrl . "\n" . $body) . '.' . $extension;
|
||||
$relativeUri = $relativeDir . '/' . $filename;
|
||||
$targetPath = $targetDir . DIRECTORY_SEPARATOR . $filename;
|
||||
|
||||
if (!is_file($targetPath) && file_put_contents($targetPath, $body) === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return FileService::getFileUrl($relativeUri);
|
||||
}
|
||||
|
||||
private function ensureLiuheAnalysisContent(CommunityPost $post, array $ext): array
|
||||
{
|
||||
$images = is_array($post->images) ? array_values($post->images) : [];
|
||||
$originalImageHash = md5(json_encode($images, JSON_UNESCAPED_UNICODE));
|
||||
$localizeResult = $this->localizeLiuhePostImagesForAi($post, $images);
|
||||
if (empty($localizeResult['success'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => (string) ($localizeResult['error'] ?? '图片下载到本地失败'),
|
||||
];
|
||||
}
|
||||
|
||||
$images = array_values($localizeResult['images']);
|
||||
$imageHash = md5(json_encode($images, JSON_UNESCAPED_UNICODE));
|
||||
$cacheSource = (string) ($ext['lottery_analysis_source'] ?? '');
|
||||
$cacheHash = (string) ($ext['lottery_analysis_images_hash'] ?? '');
|
||||
@@ -713,8 +874,16 @@ class CommunityController extends BaseApiController
|
||||
if (
|
||||
!empty($ext['lottery_analysis_content'])
|
||||
&& $cacheSource === 'image_only'
|
||||
&& $cacheHash === $imageHash
|
||||
&& in_array($cacheHash, [$imageHash, $originalImageHash], true)
|
||||
) {
|
||||
if ($cacheHash !== $imageHash) {
|
||||
$ext['lottery_analysis_images_hash'] = $imageHash;
|
||||
CommunityPost::where('id', (int) $post->id)->update([
|
||||
'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
KbSyncService::enqueue('post', 'post', (int) $post->id, 'upsert', 45);
|
||||
}
|
||||
return ['success' => true, 'ext' => $ext, 'from_cache' => true];
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use app\common\service\MTranServerService;
|
||||
|
||||
class AiService
|
||||
{
|
||||
private const LOTTERY_POST_IMAGE_TIMEOUT = 25;
|
||||
|
||||
|
||||
// 生成类 AI 统一走 DeepSeekClient 的 OpenAI Responses 配置。
|
||||
@@ -341,7 +342,7 @@ class AiService
|
||||
if (!$image) {
|
||||
return '';
|
||||
}
|
||||
return self::prepareGptImageUrl(FileService::getFileUrl($image));
|
||||
return FileService::getFileUrl($image);
|
||||
}, $images)));
|
||||
|
||||
if (empty($imageUrls)) {
|
||||
@@ -366,6 +367,10 @@ class AiService
|
||||
$options = self::gptModelOptions([
|
||||
'max_tokens' => 2000,
|
||||
'temperature' => 0.3,
|
||||
'timeout' => self::LOTTERY_POST_IMAGE_TIMEOUT,
|
||||
'reasoning_effort' => 'low',
|
||||
'retry_times' => 0,
|
||||
'retry_sleep_ms' => 0,
|
||||
]);
|
||||
|
||||
$client = new DeepSeekClient();
|
||||
@@ -380,52 +385,6 @@ class AiService
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected static function prepareGptImageUrl(string $imageUrl): string
|
||||
{
|
||||
if (!preg_match('/^https?:\/\//i', $imageUrl)) {
|
||||
return $imageUrl;
|
||||
}
|
||||
|
||||
$dataUrl = self::downloadImageAsDataUrl($imageUrl);
|
||||
return $dataUrl !== '' ? $dataUrl : $imageUrl;
|
||||
}
|
||||
|
||||
protected static function downloadImageAsDataUrl(string $imageUrl): string
|
||||
{
|
||||
$ch = curl_init($imageUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 10,
|
||||
CURLOPT_TIMEOUT => 25,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_USERAGENT => 'Mozilla/5.0 SportEraBot/1.0',
|
||||
]);
|
||||
|
||||
$body = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$contentType = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200 || !is_string($body) || $body === '' || strlen($body) > 8 * 1024 * 1024) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$mime = strtolower(trim(explode(';', $contentType)[0] ?? ''));
|
||||
if ($mime === '' || !str_starts_with($mime, 'image/')) {
|
||||
$path = parse_url($imageUrl, PHP_URL_PATH) ?: '';
|
||||
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
$mime = match ($ext) {
|
||||
'png' => 'image/png',
|
||||
'webp' => 'image/webp',
|
||||
'gif' => 'image/gif',
|
||||
default => 'image/jpeg',
|
||||
};
|
||||
}
|
||||
|
||||
return 'data:' . $mime . ';base64,' . base64_encode($body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存分析结果
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,8 @@ use app\common\model\ai\AiConfig;
|
||||
|
||||
class KbRagService
|
||||
{
|
||||
private const LOTTERY_POST_ANALYSIS_TIMEOUT = 25;
|
||||
|
||||
public static function analyzeMatch(int $matchId, array $matchData, int $userId = 0): array
|
||||
{
|
||||
$retrieval = self::retrieveMatchEvidence($matchId, $matchData, $userId, 'match_analysis');
|
||||
@@ -212,6 +214,10 @@ class KbRagService
|
||||
return DeepSeekClient::defaultModelOptions([
|
||||
'max_tokens' => 1200,
|
||||
'temperature' => 0.35,
|
||||
'timeout' => self::LOTTERY_POST_ANALYSIS_TIMEOUT,
|
||||
'reasoning_effort' => 'low',
|
||||
'retry_times' => 0,
|
||||
'retry_sleep_ms' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user