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) . '...'; } }