mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
feat: 新闻插件开发, 使用function_call支持播放当天新闻 #403
This commit is contained in:
@@ -94,10 +94,11 @@ Intent:
|
||||
type: nointent
|
||||
# plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用
|
||||
# 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载
|
||||
# 下面是加载查天气、角色切换的插件示例
|
||||
# 下面是加载查天气、角色切换、加载查新闻的插件示例
|
||||
functions:
|
||||
- change_role
|
||||
- get_weather
|
||||
- get_news
|
||||
|
||||
# 插件的基础配置
|
||||
plugins:
|
||||
@@ -106,6 +107,14 @@ plugins:
|
||||
# 想稳定一点就自行申请替换,每天有1000次免费调用
|
||||
# 申请地址:https://console.qweather.com/#/apps/create-key/over
|
||||
get_weather: { "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" }
|
||||
# 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻
|
||||
# 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/
|
||||
get_news:
|
||||
default_rss_url: "https://www.chinanews.com.cn/rss/society.xml"
|
||||
category_urls:
|
||||
society: "https://www.chinanews.com.cn/rss/society.xml"
|
||||
world: "https://www.chinanews.com.cn/rss/world.xml"
|
||||
finance: "https://www.chinanews.com.cn/rss/finance.xml"
|
||||
|
||||
Memory:
|
||||
mem0ai:
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import random
|
||||
import requests
|
||||
import xml.etree.ElementTree as ET
|
||||
from bs4 import BeautifulSoup
|
||||
from config.logger import setup_logging
|
||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
GET_NEWS_FUNCTION_DESC = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_news",
|
||||
"description": (
|
||||
"获取最新新闻,随机选择一条新闻进行播报。"
|
||||
"用户可以指定新闻类型,如社会新闻、科技新闻、国际新闻等。"
|
||||
"如果没有指定,默认播报社会新闻。"
|
||||
"用户可以要求获取详细内容,此时会获取新闻的详细内容。"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "新闻类别,例如社会、科技、国际。可选参数,如果不提供则使用默认类别"
|
||||
},
|
||||
"detail": {
|
||||
"type": "boolean",
|
||||
"description": "是否获取详细内容,默认为false。如果为true,则获取上一条新闻的详细内容"
|
||||
},
|
||||
"lang": {
|
||||
"type": "string",
|
||||
"description": "返回用户使用的语言code,例如zh_CN/zh_HK/en_US/ja_JP等,默认zh_CN"
|
||||
}
|
||||
},
|
||||
"required": ["lang"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def fetch_news_from_rss(rss_url):
|
||||
"""从RSS源获取新闻列表"""
|
||||
try:
|
||||
response = requests.get(rss_url)
|
||||
response.raise_for_status()
|
||||
|
||||
# 解析XML
|
||||
root = ET.fromstring(response.content)
|
||||
|
||||
# 查找所有item元素(新闻条目)
|
||||
news_items = []
|
||||
for item in root.findall('.//item'):
|
||||
title = item.find('title').text if item.find('title') is not None else "无标题"
|
||||
link = item.find('link').text if item.find('link') is not None else "#"
|
||||
description = item.find('description').text if item.find('description') is not None else "无描述"
|
||||
pubDate = item.find('pubDate').text if item.find('pubDate') is not None else "未知时间"
|
||||
|
||||
news_items.append({
|
||||
'title': title,
|
||||
'link': link,
|
||||
'description': description,
|
||||
'pubDate': pubDate
|
||||
})
|
||||
|
||||
return news_items
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"获取RSS新闻失败: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def fetch_news_detail(url):
|
||||
"""获取新闻详情页内容并总结"""
|
||||
try:
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
# 尝试提取正文内容 (这里的选择器需要根据实际网站结构调整)
|
||||
content_div = soup.select_one('.content_desc, .content, article, .article-content')
|
||||
if content_div:
|
||||
paragraphs = content_div.find_all('p')
|
||||
content = '\n'.join([p.get_text().strip() for p in paragraphs if p.get_text().strip()])
|
||||
return content
|
||||
else:
|
||||
# 如果找不到特定的内容区域,尝试获取所有段落
|
||||
paragraphs = soup.find_all('p')
|
||||
content = '\n'.join([p.get_text().strip() for p in paragraphs if p.get_text().strip()])
|
||||
return content[:2000] # 限制长度
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"获取新闻详情失败: {e}")
|
||||
return "无法获取详细内容"
|
||||
|
||||
|
||||
def map_category(category_text):
|
||||
"""将用户输入的中文类别映射到配置文件中的类别键"""
|
||||
if not category_text:
|
||||
return None
|
||||
|
||||
# 类别映射字典,目前支持社会、国际、财经新闻,如需更多类型,参见配置文件
|
||||
category_map = {
|
||||
# 社会新闻
|
||||
"社会": "society",
|
||||
"社会新闻": "society",
|
||||
# 国际新闻
|
||||
"国际": "world",
|
||||
"国际新闻": "world",
|
||||
# 财经新闻
|
||||
"财经": "finance",
|
||||
"财经新闻": "finance",
|
||||
"金融": "finance",
|
||||
"经济": "finance"
|
||||
}
|
||||
|
||||
# 转换为小写并去除空格
|
||||
normalized_category = category_text.lower().strip()
|
||||
|
||||
# 返回映射结果,如果没有匹配项则返回原始输入
|
||||
return category_map.get(normalized_category, category_text)
|
||||
|
||||
|
||||
@register_function('get_news', GET_NEWS_FUNCTION_DESC, ToolType.SYSTEM_CTL)
|
||||
def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_CN"):
|
||||
"""获取新闻并随机选择一条进行播报,或获取上一条新闻的详细内容"""
|
||||
try:
|
||||
# 如果detail为True,获取上一条新闻的详细内容
|
||||
if detail:
|
||||
if not hasattr(conn, 'last_news_link') or not conn.last_news_link or 'link' not in conn.last_news_link:
|
||||
return ActionResponse(Action.REQLLM, "抱歉,没有找到最近查询的新闻,请先获取一条新闻。", None)
|
||||
|
||||
link = conn.last_news_link.get('link')
|
||||
title = conn.last_news_link.get('title', '未知标题')
|
||||
|
||||
if link == '#':
|
||||
return ActionResponse(Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", None)
|
||||
|
||||
logger.bind(tag=TAG).debug(f"获取新闻详情: {title}, URL={link}")
|
||||
|
||||
# 获取新闻详情
|
||||
detail_content = fetch_news_detail(link)
|
||||
|
||||
if not detail_content or detail_content == "无法获取详细内容":
|
||||
return ActionResponse(Action.REQLLM, f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。", None)
|
||||
|
||||
# 构建详情报告
|
||||
detail_report = (
|
||||
f"根据下列数据,用{lang}回应用户的新闻详情查询请求:\n\n"
|
||||
f"新闻标题: {title}\n"
|
||||
f"详细内容: {detail_content}\n\n"
|
||||
f"(请对上述新闻内容进行总结,提取关键信息,以自然、流畅的方式向用户播报,"
|
||||
f"不要提及这是总结,就像是在讲述一个完整的新闻故事)"
|
||||
)
|
||||
|
||||
return ActionResponse(Action.REQLLM, detail_report, None)
|
||||
|
||||
# 否则,获取新闻列表并随机选择一条
|
||||
# 从配置中获取RSS URL
|
||||
rss_config = conn.config["plugins"]["get_news"]
|
||||
default_rss_url = rss_config.get("default_rss_url", "https://www.chinanews.com.cn/rss/society.xml")
|
||||
|
||||
# 将用户输入的类别映射到配置中的类别键
|
||||
mapped_category = map_category(category)
|
||||
|
||||
# 如果提供了类别,尝试从配置中获取对应的URL
|
||||
rss_url = default_rss_url
|
||||
if mapped_category and mapped_category in rss_config.get("category_urls", {}):
|
||||
rss_url = rss_config["category_urls"][mapped_category]
|
||||
|
||||
logger.bind(tag=TAG).info(f"获取新闻: 原始类别={category}, 映射类别={mapped_category}, URL={rss_url}")
|
||||
|
||||
# 获取新闻列表
|
||||
news_items = fetch_news_from_rss(rss_url)
|
||||
|
||||
if not news_items:
|
||||
return ActionResponse(Action.REQLLM, "抱歉,未能获取到新闻信息,请稍后再试。", None)
|
||||
|
||||
# 随机选择一条新闻
|
||||
selected_news = random.choice(news_items)
|
||||
|
||||
# 保存当前新闻链接到连接对象,以便后续查询详情
|
||||
if not hasattr(conn, 'last_news_link'):
|
||||
conn.last_news_link = {}
|
||||
conn.last_news_link = {
|
||||
'link': selected_news.get('link', '#'),
|
||||
'title': selected_news.get('title', '未知标题')
|
||||
}
|
||||
|
||||
# 构建新闻报告
|
||||
news_report = (
|
||||
f"根据下列数据,用{lang}回应用户的新闻查询请求:\n\n"
|
||||
f"新闻标题: {selected_news['title']}\n"
|
||||
f"发布时间: {selected_news['pubDate']}\n"
|
||||
f"新闻内容: {selected_news['description']}\n"
|
||||
f"(请以自然、流畅的方式向用户播报这条新闻,可以适当总结内容,"
|
||||
f"直接读出新闻即可,不需要额外多余的内容。"
|
||||
f"如果用户询问更多详情,告知用户可以说'请详细介绍这条新闻'获取更多内容)"
|
||||
)
|
||||
|
||||
return ActionResponse(Action.REQLLM, news_report, None)
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"获取新闻出错: {e}")
|
||||
return ActionResponse(Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", None)
|
||||
Reference in New Issue
Block a user