← 返回聊天
新建
删除
Models
gpt5.php
gpt5_file.php
gpt5_mini_file.php
openai_chat.php
Tools
get_time.php
get_weather.php
global_search_messages.php
math.php
memo.php
news.php
search_arxiv.php
search_crossref.php
search_github_code.php
search_pubmed.php
search_semantic_scholar.php
stock_market.php
url.php
<?php declare(strict_types=1); /** * 新闻工具(无需Key): * - RSS:BBC 等 RSS -> 解析 XML -> JSON * - HN:Hacker News Algolia Search API -> JSON(无需Key) * * 设计目标:群聊里“随手拉点新闻话题”,轻量稳定,不追求全网聚合。 * 为避免与其他工具冲突:所有辅助函数加前缀 news_。 */ function news_http_get(string $url, int $timeoutSec = 12, array $headers = []): array { $ch = curl_init($url); if ($ch === false) throw new RuntimeException('curl_init failed'); $baseHeaders = array_merge([ 'User-Agent: Mozilla/5.0 (compatible; ai-chat-tool/1.0)', 'Accept: */*', ], $headers); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => $timeoutSec, CURLOPT_TIMEOUT => $timeoutSec, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_HTTPHEADER => $baseHeaders, CURLOPT_ENCODING => '', ]); $resp = curl_exec($ch); $errno = curl_errno($ch); $err = curl_error($ch); $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($errno !== 0) return ['ok' => false, 'status' => 0, 'body' => '', 'error' => 'cURL error: ' . $err]; if ($status < 200 || $status >= 300) return ['ok' => false, 'status' => $status, 'body' => (string)$resp, 'error' => 'HTTP ' . $status]; return ['ok' => true, 'status' => $status, 'body' => (string)$resp, 'error' => '']; } function news_http_get_json(string $url, int $timeoutSec = 12): array { $r = news_http_get($url, $timeoutSec, ['Accept: application/json']); if (!$r['ok']) throw new RuntimeException($r['error']); $data = json_decode($r['body'], true); if (!is_array($data)) throw new RuntimeException('Invalid JSON'); return $data; } function news_strip(string $s): string { $s = html_entity_decode($s, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $s = preg_replace('/\s+/u', ' ', $s) ?? $s; return trim($s); } function news_parse_rss(string $xml, int $limit = 10): array { // 兼容 RSS2.0 / Atom(简单处理) libxml_use_internal_errors(true); $sx = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA); if ($sx === false) throw new RuntimeException('Invalid XML'); $items = []; // RSS 2.0: <rss><channel><item>... if (isset($sx->channel->item)) { foreach ($sx->channel->item as $it) { $title = news_strip((string)($it->title ?? '')); $link = news_strip((string)($it->link ?? '')); $desc = news_strip((string)($it->description ?? '')); $pub = news_strip((string)($it->pubDate ?? '')); if ($title === '' && $link === '') continue; $items[] = [ 'title' => $title, 'url' => $link, 'summary' => $desc, 'published' => $pub, ]; if (count($items) >= $limit) break; } return $items; } // Atom: <feed><entry>... if (isset($sx->entry)) { foreach ($sx->entry as $en) { $title = news_strip((string)($en->title ?? '')); $link = ''; if (isset($en->link)) { foreach ($en->link as $l) { $href = (string)($l['href'] ?? ''); if ($href !== '') { $link = $href; break; } } } $summary = news_strip((string)($en->summary ?? $en->content ?? '')); $pub = news_strip((string)($en->updated ?? $en->published ?? '')); if ($title === '' && $link === '') continue; $items[] = [ 'title' => $title, 'url' => $link, 'summary' => $summary, 'published' => $pub, ]; if (count($items) >= $limit) break; } return $items; } return []; } function news_filter_by_query(array $items, string $query): array { $q = mb_strtolower(trim($query)); if ($q === '') return $items; $out = []; foreach ($items as $it) { $hay = mb_strtolower(($it['title'] ?? '') . ' ' . ($it['summary'] ?? '')); if (mb_strpos($hay, $q) !== false) $out[] = $it; } return $out; } return [ 'name' => 'get_news', 'description' => '获取新闻(默认无需Key):支持 RSS 聚合(BBC等)与 Hacker News(Algolia,无Key)。', 'parameters' => [ 'type' => 'object', 'properties' => [ 'source' => [ 'type' => 'string', 'description' => '新闻源:bbc_world / bbc_top / hn_frontpage / custom_rss', 'default' => 'bbc_world' ], 'limit' => [ 'type' => 'integer', 'description' => '返回条数,默认 8,最大 20', 'default' => 8 ], 'query' => [ 'type' => 'string', 'description' => '可选:关键词过滤(在标题/摘要里包含即可)', ], 'rss_url' => [ 'type' => 'string', 'description' => '当 source=custom_rss 时必填:RSS 地址' ], ], 'required' => ['source'], ], 'run' => function(array $args, array $context) { $source = trim((string)($args['source'] ?? 'bbc_world')); $limit = (int)($args['limit'] ?? 8); if ($limit <= 0) $limit = 8; if ($limit > 20) $limit = 20; $query = trim((string)($args['query'] ?? '')); // 预置源 $rssMap = [ 'bbc_world' => 'https://feeds.bbci.co.uk/news/world/rss.xml', 'bbc_top' => 'https://feeds.bbci.co.uk/news/rss.xml', ]; if ($source === 'hn_frontpage') { // HN front_page(Algolia,无Key) $url = 'https://hn.algolia.com/api/v1/search?tags=front_page'; $data = news_http_get_json($url, 12); $hits = $data['hits'] ?? []; $items = []; if (is_array($hits)) { foreach ($hits as $h) { if (!is_array($h)) continue; $title = news_strip((string)($h['title'] ?? $h['story_title'] ?? '')); $link = news_strip((string)($h['url'] ?? $h['story_url'] ?? '')); $author = news_strip((string)($h['author'] ?? '')); $created = $h['created_at'] ?? ''; $summary = $author !== '' ? ('by ' . $author) : ''; if ($title === '' && $link === '') continue; $items[] = [ 'title' => $title, 'url' => $link, 'summary' => $summary, 'published' => (string)$created, ]; if (count($items) >= $limit) break; } } $items = news_filter_by_query($items, $query); return [ 'ok' => true, 'source' => 'hn_frontpage', 'endpoint' => $url, 'count' => count($items), 'items' => $items, 'note' => 'HN Search API(Algolia)无Key,但有速率限制。', ]; } // RSS 模式 $rssUrl = ''; if ($source === 'custom_rss') { $rssUrl = trim((string)($args['rss_url'] ?? '')); if ($rssUrl === '') throw new RuntimeException('source=custom_rss 时 rss_url 不能为空'); } elseif (isset($rssMap[$source])) { $rssUrl = $rssMap[$source]; } else { throw new RuntimeException('未知 source:' . $source); } $r = news_http_get($rssUrl, 12, ['Accept: application/rss+xml, application/xml;q=0.9, */*;q=0.8']); if (!$r['ok']) { return ['ok' => false, 'error' => $r['error'], 'source' => $source, 'endpoint' => $rssUrl]; } $items = news_parse_rss($r['body'], $limit); $items = news_filter_by_query($items, $query); return [ 'ok' => true, 'source' => $source, 'endpoint' => $rssUrl, 'count' => count($items), 'items' => $items, 'note' => 'RSS 无Key,稳定;不同站点的发布时间字段格式可能不同。', ]; }, ];
保存