deploy: auto commit server changes 2026-06-12 17:18:58
This commit is contained in:
@@ -33,7 +33,7 @@ class AiAssistantContextService
|
||||
$hits = self::retrieveKnowledge($message, $userId, $sessionId, $topK, $plan);
|
||||
$matchItems = self::searchMatches($message, $plan);
|
||||
$cryptoItems = self::resolveCryptoContext($message, $plan);
|
||||
$payload = self::composePayload($hits, $matchItems, $cryptoItems, $plan);
|
||||
$payload = self::composePayload($hits, $matchItems, $cryptoItems, [], $plan);
|
||||
|
||||
return array_merge($payload, [
|
||||
'knowledge_hits' => $hits,
|
||||
@@ -42,9 +42,18 @@ class AiAssistantContextService
|
||||
]);
|
||||
}
|
||||
|
||||
public static function composePayload(array $hits, array $matchItems, array $cryptoItems, array $plan = []): array
|
||||
public static function composePayload(
|
||||
array $hits,
|
||||
array $matchItems,
|
||||
array $cryptoItems,
|
||||
array $webNewsItems = [],
|
||||
array $plan = []
|
||||
): array
|
||||
{
|
||||
$sources = [];
|
||||
foreach ($webNewsItems as $item) {
|
||||
$sources[] = $item['source'];
|
||||
}
|
||||
foreach ($hits as $hit) {
|
||||
$sources[] = self::sourceFromKbHit($hit);
|
||||
}
|
||||
@@ -58,6 +67,7 @@ class AiAssistantContextService
|
||||
$sources = self::uniqueSources($sources);
|
||||
$context = [
|
||||
'query_plan' => self::compactPlan($plan),
|
||||
'web_news_context' => array_column($webNewsItems, 'context'),
|
||||
'knowledge_hits' => array_map([self::class, 'compactHit'], $hits),
|
||||
'match_context' => array_column($matchItems, 'context'),
|
||||
'crypto_realtime' => array_column($cryptoItems, 'context'),
|
||||
@@ -66,7 +76,7 @@ class AiAssistantContextService
|
||||
return [
|
||||
'context_text' => json_encode($context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
||||
'sources' => array_values($sources),
|
||||
'has_context' => !empty($hits) || !empty($matchItems) || !empty($cryptoItems),
|
||||
'has_context' => !empty($webNewsItems) || !empty($hits) || !empty($matchItems) || !empty($cryptoItems),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -221,6 +221,9 @@ class AiAssistantPlanner
|
||||
if (preg_match('/BTC|ETH|BNB|SOL|XRP|DOGE|TON|TRX|ADA|AVAX|比特币|以太坊|行情/u', $message)) {
|
||||
return 'crypto_quote';
|
||||
}
|
||||
if (preg_match('/资讯|新闻|文章|帖子|社区|消息|转会/u', $message)) {
|
||||
return 'news_search';
|
||||
}
|
||||
if (preg_match('/赛程|今天|今日|明天|下一场|下场|即将|未开赛/u', $message)) {
|
||||
return 'match_schedule';
|
||||
}
|
||||
@@ -230,9 +233,6 @@ class AiAssistantPlanner
|
||||
if (preg_match('/六合|彩票|号码|特码|开奖结果|开奖记录/u', $message)) {
|
||||
return 'lottery_analysis';
|
||||
}
|
||||
if (preg_match('/资讯|新闻|文章|帖子|社区/u', $message)) {
|
||||
return 'news_search';
|
||||
}
|
||||
return 'general_search';
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ class AiAssistantRanker
|
||||
$trace->rankedHits,
|
||||
$trace->retrievals['match_items'] ?? [],
|
||||
$trace->retrievals['crypto_items'] ?? [],
|
||||
$trace->retrievals['web_news_items'] ?? [],
|
||||
$trace->plan
|
||||
);
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ class AiAssistantResponder
|
||||
'role' => 'user',
|
||||
'content' => "用户问题:\n{$message}\n\n查询理解结果:\n"
|
||||
. $planSummary
|
||||
. "\n\n站内资料与实时上下文:\n"
|
||||
. "\n\n站内资料、联网资讯与实时上下文:\n"
|
||||
. ($contextText ?: '{}')
|
||||
. "\n\n请按照“直接结论、依据摘要、相关内容、风险提示”的结构回答。"
|
||||
. "如果站内资料不足,请明确说明,不要编造。彩票、赛事预测、加密行情必须提示不构成投资或购彩建议。",
|
||||
|
||||
@@ -7,13 +7,15 @@ class AiAssistantRetriever
|
||||
public static function retrieve(AiAssistantTrace $trace, int $userId, int $sessionId): AiAssistantTrace
|
||||
{
|
||||
$context = AiAssistantContextService::build($trace->originalMessage, $userId, $sessionId, $trace->plan);
|
||||
$webNewsItems = AiAssistantWebNewsRetriever::retrieve($trace->originalMessage, $trace->plan, $trace->historyMessages);
|
||||
|
||||
$trace->retrievals = [
|
||||
'knowledge_hits' => $context['knowledge_hits'] ?? [],
|
||||
'match_items' => $context['match_items'] ?? [],
|
||||
'crypto_items' => $context['crypto_items'] ?? [],
|
||||
'web_news_items' => $webNewsItems,
|
||||
];
|
||||
$trace->hasContext = !empty($context['has_context']);
|
||||
$trace->hasContext = !empty($context['has_context']) || !empty($webNewsItems);
|
||||
|
||||
return $trace;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ class AiAssistantTrace
|
||||
'knowledge_hits' => [],
|
||||
'match_items' => [],
|
||||
'crypto_items' => [],
|
||||
'web_news_items' => [],
|
||||
];
|
||||
public array $rankedHits = [];
|
||||
public array $finalSources = [];
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service\ai;
|
||||
|
||||
use app\common\model\ai\AiConfig;
|
||||
|
||||
class AiAssistantWebNewsRetriever
|
||||
{
|
||||
public static function retrieve(string $message, array $plan, array $historyMessages = []): array
|
||||
{
|
||||
if (!self::shouldRetrieve($message, $plan)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$client = new DeepSeekClient();
|
||||
$result = $client->chatJson(self::systemPrompt(), self::userPrompt($message, $plan, $historyMessages), self::modelOptions());
|
||||
if (empty($result['success']) || !is_array($result['parsed'] ?? null)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return self::normalizeItems($result['parsed'], $message);
|
||||
}
|
||||
|
||||
private static function shouldRetrieve(string $message, array $plan): bool
|
||||
{
|
||||
if ((int) AiConfig::getVal('assistant_enable_web_news_search', '1') !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$intent = (string) ($plan['intent'] ?? '');
|
||||
$preferredDomain = (string) ($plan['preferred_domain'] ?? '');
|
||||
$domains = (array) ($plan['domains'] ?? []);
|
||||
|
||||
if (in_array($intent, ['crypto_quote', 'match_schedule', 'match_result', 'lottery_analysis'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($preferredDomain === 'article' || in_array('article', $domains, true) || in_array('post', $domains, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (bool) preg_match('/资讯|新闻|消息|报道|热点|刚刚|最新|头条/u', $message);
|
||||
}
|
||||
|
||||
private static function systemPrompt(): string
|
||||
{
|
||||
$today = date('Y-m-d');
|
||||
return '你是世博头条 AI 助手的联网资讯检索器。'
|
||||
. '请调用 web search 工具搜索与用户问题最相关、尽量最新的体育资讯或新闻。'
|
||||
. '今天日期是 ' . $today . '。'
|
||||
. '只输出 JSON:{"items":[{"title":"","source":"","url":"","summary":"","published_at":""}]}。'
|
||||
. '要求:最多返回 5 条;title 和 url 必填;summary 只写 1-2 句;没有可靠结果时返回 {"items":[]}。';
|
||||
}
|
||||
|
||||
private static function userPrompt(string $message, array $plan, array $historyMessages): string
|
||||
{
|
||||
$history = [];
|
||||
foreach (array_slice($historyMessages, -4) as $row) {
|
||||
$content = trim((string) ($row['content'] ?? ''));
|
||||
if ($content === '') {
|
||||
continue;
|
||||
}
|
||||
$history[] = [
|
||||
'role' => (string) ($row['role'] ?? 'user'),
|
||||
'content' => mb_substr($content, 0, 200),
|
||||
];
|
||||
}
|
||||
|
||||
return "用户问题:\n"
|
||||
. $message
|
||||
. "\n\n查询规划:\n"
|
||||
. json_encode([
|
||||
'intent' => $plan['intent'] ?? '',
|
||||
'rewritten_query' => $plan['rewritten_query'] ?? '',
|
||||
'alternate_queries' => $plan['alternate_queries'] ?? [],
|
||||
'entities' => $plan['entities'] ?? [],
|
||||
'date_range' => $plan['date_range'] ?? [],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
||||
. "\n\n最近对话:\n"
|
||||
. json_encode($history, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
||||
. "\n\n请检索最新外网资讯并输出 JSON。";
|
||||
}
|
||||
|
||||
private static function modelOptions(): array
|
||||
{
|
||||
return DeepSeekClient::defaultModelOptions([
|
||||
'max_tokens' => 900,
|
||||
'temperature' => 0.2,
|
||||
'retry_times' => 1,
|
||||
'retry_sleep_ms' => 600,
|
||||
'tools' => [
|
||||
['type' => 'web_search'],
|
||||
],
|
||||
'tool_choice' => 'required',
|
||||
'include' => ['web_search_call.action.sources'],
|
||||
]);
|
||||
}
|
||||
|
||||
private static function normalizeItems(array $parsed, string $message): array
|
||||
{
|
||||
$items = [];
|
||||
foreach ((array) ($parsed['items'] ?? []) as $row) {
|
||||
$title = trim((string) ($row['title'] ?? ''));
|
||||
$url = trim((string) ($row['url'] ?? ''));
|
||||
if ($title === '' || $url === '' || !preg_match('#^https?://#i', $url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sourceName = trim((string) ($row['source'] ?? ''));
|
||||
$publishedAt = trim((string) ($row['published_at'] ?? ''));
|
||||
$summary = trim((string) ($row['summary'] ?? ''));
|
||||
$contextSummary = trim(implode(' ', array_filter([
|
||||
$sourceName !== '' ? '来源:' . $sourceName : '',
|
||||
$publishedAt !== '' ? '时间:' . $publishedAt : '',
|
||||
$summary,
|
||||
])));
|
||||
|
||||
$items[] = [
|
||||
'context' => [
|
||||
'domain' => 'web_news',
|
||||
'title' => $title,
|
||||
'summary' => $contextSummary !== '' ? $contextSummary : $message,
|
||||
'url' => $url,
|
||||
'source' => $sourceName,
|
||||
'published_at' => $publishedAt,
|
||||
],
|
||||
'source' => [
|
||||
'type' => 'web_news',
|
||||
'title' => $title,
|
||||
'summary' => self::clipText($contextSummary !== '' ? $contextSummary : $summary, 140),
|
||||
'path' => '/pages/webview/webview?url=' . urlencode($url),
|
||||
'source_id' => 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return array_slice($items, 0, 5);
|
||||
}
|
||||
|
||||
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) . '...';
|
||||
}
|
||||
}
|
||||
@@ -304,6 +304,15 @@ class DeepSeekClient
|
||||
if (!empty($instructions)) {
|
||||
$payload['instructions'] = implode("\n\n", $instructions);
|
||||
}
|
||||
if (!empty($options['tools']) && is_array($options['tools'])) {
|
||||
$payload['tools'] = array_values($options['tools']);
|
||||
}
|
||||
if (!empty($options['tool_choice'])) {
|
||||
$payload['tool_choice'] = $options['tool_choice'];
|
||||
}
|
||||
if (!empty($options['include']) && is_array($options['include'])) {
|
||||
$payload['include'] = array_values($options['include']);
|
||||
}
|
||||
$reasoningEffort = trim((string) ($options['reasoning_effort'] ?? ''));
|
||||
if ($reasoningEffort !== '') {
|
||||
$payload['reasoning'] = ['effort' => $reasoningEffort];
|
||||
|
||||
Reference in New Issue
Block a user