diff --git a/README.md b/README.md index 4eafc84a..173678c7 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ docker-compose -f docker-compose.yml up - [x] GitHub 趋势 - [x] 单词词语翻译(不支持定时任务) - [x] 少数派早报 -- [x] 历史上的今天 +- [x] 每日环球视野 - [x] 二维码生成 - [x] 待办清单(不支持定时任务) - [x] 人民日报 PDF @@ -152,11 +152,25 @@ docker-compose -f docker-compose.yml up ### ⚙️ LLM 配置 +#### 1.OpenAi + | 配置项 | 解释 | 备注 | | --- | --- | --- | | `openai_base_api` | OpenAI 服务的 BaseAPI | 默认为 `https://api.openai.com` | | `openai_token` | OpenAI Token(Key) | 以 `sk_` 开头的字符串密钥 | +#### 2.Spark(讯飞星火大模型) + +##### 现免费领取Spark 4.0 Ultra,tokens: 200万,有效期:1年 + +获取途径:[讯飞星火大模型-AI大语言模型-星火大模型-科大讯飞](https://xinghuo.xfyun.cn/sparkapi?scr=price) + +| 配置项 | 解释 | 备注 | +| ------------- | ------------------------ | ------------------------------------------------------------ | +| `spark_api` | 星火大模型服务的 BaseAPI | 默认为`https://spark-api-open.xf-yun.com/v1/chat/completions` | +| `spark_model` | 指定访问的模型版本 | lite指向Lite版本;4.0Ultra指向4.0 Ultra版本; | +| `spark_token` | 星火大模型的Token | 字符串密钥 | + ### ⚙️ GitHub Webhook 配置 | 配置项 | 解释 | 备注 | @@ -227,7 +241,7 @@ docker-compose -f docker-compose.yml up 日志文件存放在项目根目录下的 `logs/` 文件夹中。 默认的日志记录级别为 `INFO`,日志记录级别可选值有 `DEBUG`、`INFO`、`WARNING`、`ERROR`、`CRITICAL`。 - + ### Docker Compose 部署时 若需要调整日志记录级别,请修改 `docker-compose.yml` 文件中的 `WECHATTER_LOG_LEVEL` 环境变量。 diff --git a/config.yaml.example b/config.yaml.example index 447f2f3c..fb08f045 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -33,6 +33,10 @@ ban_group_list: [ ] openai_base_api: https://api.openai.com openai_token: sk_your_openai_token +spark_api: https://spark-api-open.xf-yun.com/v1/chat/completions +spark_model: 4.0Ultra +spark_token: your_spark_token + # GitHub Webhook github_webhook_enabled: True @@ -99,4 +103,4 @@ discord_message_forwarding_rule_list: # GPT Mode Person gpt_mode_person_list: [ ] -gpt_mode_model: "gpt4" \ No newline at end of file +gpt_mode_model: "gpt4" diff --git a/docs/command_show.md b/docs/command_show.md index cd5cd250..3df467aa 100644 --- a/docs/command_show.md +++ b/docs/command_show.md @@ -33,7 +33,7 @@ - [食物热量](#食物热量) - [中石化92号汽油指导价](#中石化92号汽油指导价) - [冷知识](#冷知识) -- [历史上的今天](#历史上的今天) +- [每日环球视野](#每日环球视野) ## 游戏基本命令 @@ -155,6 +155,6 @@ ![冷知识](./images/cmd_trivia.png) -## 历史上的今天 +## 每日环球视野 -![历史上的今天](./images/cmd_today_in_history.png) \ No newline at end of file +![每日环球视野](./images/cmd_idaily.png) diff --git a/docs/custom_command_key_config_detail.md b/docs/custom_command_key_config_detail.md index 59814d20..e00b5d8c 100644 --- a/docs/custom_command_key_config_detail.md +++ b/docs/custom_command_key_config_detail.md @@ -31,7 +31,7 @@ - TODO - `todo`: 添加待办事项 - `todo-remove`: 删除待办事项 -- `today-in-history`: 获取历史上的今天 +- `idaily`: 获取每日环球视野 - `trivia`: 获取笑话 - `weather`: 查询天气预报 - `weibo-hot`: 获取微博热搜 diff --git a/docs/images/cmd_idaily.png b/docs/images/cmd_idaily.png new file mode 100644 index 00000000..d4e68348 Binary files /dev/null and b/docs/images/cmd_idaily.png differ diff --git a/docs/images/cmd_today_in_history.png b/docs/images/cmd_today_in_history.png deleted file mode 100644 index da700414..00000000 Binary files a/docs/images/cmd_today_in_history.png and /dev/null differ diff --git a/tests/commands/test_today_in_history/__init__.py b/tests/commands/test_idaily/__init__.py similarity index 100% rename from tests/commands/test_today_in_history/__init__.py rename to tests/commands/test_idaily/__init__.py diff --git a/tests/commands/test_idaily/idaily_response.json b/tests/commands/test_idaily/idaily_response.json new file mode 100644 index 00000000..084f826e --- /dev/null +++ b/tests/commands/test_idaily/idaily_response.json @@ -0,0 +1,86 @@ +[ + { + "guid": 218572, + "type": 1, + "cat": "6", + "cover_thumb": "http://pic.yupoo.com/fotomag/7aa701a9/b54ad0d4.jpg", + "cover_sq": "http://pic.yupoo.com/fotomag/30d0f2dd/aff88815.jpg", + "cover_sq_hd": "http://pic.yupoo.com/fotomag/30d0f2dd/aff88815.jpg", + "cover_landscape": "http://pic.yupoo.com/fotomag/ceb7f2e4/4b506571.jpg", + "cover_landscape_hd": "http://pic.yupoo.com/fotomag/4600d642/c681b939.jpg", + "pubdate": "November 17, 2024", + "archive_timestamp": 1731772800, + "pubdate_timestamp": 1731842880, + "lastupdate_timestamp": 1731854888, + "ui_sets": { + "caption_subtitle": "泰国庆祝天灯节", + "cover_landscape_hd_4k": "http://pic.yupoo.com/fotomag/2f5642d1/b1a5daf7.jpg" + }, + "title": "November 17, 2024", + "author": "", + "source": "", + "link_share": "https://m.idai.ly/se/8eeShU", + "link_wechat": "https://m.idai.ly/se/8eeShU", + "title_wechat_tml": "泰国庆祝天灯节 - November 17, 2024 | iDaily 每日全球最佳新闻图片", + "has_caption": 1, + "has_news": 1, + "latitude": 18.7964642, + "longitude": 98.6600586, + "geo_span": 0.25, + "location": "泰国 · 清迈", + "summary": "", + "content": "泰国民众放飞孔明灯庆祝「天灯节」(Yi Peng festival),清迈。「天灯节」是泰国北部地区的传统节日,历史可追溯至13世纪泰北兰纳王国时期,庆祝日期为每年泰国农历第12个月的满月日,人们会放飞孔明灯庆祝新一年即将开始。泰国旅游部数据显示2024年1至10月接待外国游客超过2900万人次,创造1.35万亿泰铢(约合393亿美元)旅游业收入,前5大游客来源国依次为中国、马来西亚、印度、韩国和俄罗斯。摄影师:Manan Vatsyayana", + "coordinate_sets": [], + "entry_imgs": [], + "tags": [ + { + "id": "culture", + "name": "CULTURE · 人类文化", + "focus": 1 + } + ], + "news_count": 6 + }, + { + "guid": 218571, + "type": 1, + "cat": "6", + "cover_thumb": "http://pic.yupoo.com/fotomag/0fb2e5d0/790f0d7b.jpg", + "cover_sq": "http://pic.yupoo.com/fotomag/c1974685/8395c708.jpg", + "cover_sq_hd": "http://pic.yupoo.com/fotomag/c1974685/8395c708.jpg", + "cover_landscape": "http://pic.yupoo.com/fotomag/e960e6e1/b846d124.jpg", + "cover_landscape_hd": "http://pic.yupoo.com/fotomag/c8c52912/a379ccf6.jpg", + "pubdate": "November 17, 2024", + "archive_timestamp": 1731772800, + "pubdate_timestamp": 1731841140, + "lastupdate_timestamp": 1731854900, + "ui_sets": { + "caption_subtitle": "中美元首利马会晤", + "cover_landscape_hd_4k": "http://pic.yupoo.com/fotomag/dc420fab/e6a3dff6.jpg" + }, + "title": "November 17, 2024", + "author": "", + "source": "", + "link_share": "https://m.idai.ly/se/8e7445", + "link_wechat": "https://m.idai.ly/se/8e7445", + "title_wechat_tml": "中美元首利马会晤 - November 17, 2024 | iDaily 每日全球最佳新闻图片", + "has_caption": 1, + "has_news": 1, + "latitude": -12.0466888, + "longitude": -77.0430886, + "geo_span": 0.25, + "location": "秘鲁 · 利马", + "summary": "", + "content": "中国国家主席习近平与美国总统 Joe Biden 在 APEC 峰会期间举行会晤,秘鲁利马。11月16日,中美两国领导人就双边关系、人工智能治理、地区及国际局势议题举行1小时45分钟会谈。习近平就台湾问题、中国南海、经贸科技、网络安全、乌克兰危机、朝鲜半岛局势等重大问题阐明中方立场。Biden 强调美国的一个中国政策保持不变,对中国支持俄罗斯国防工业深表关切,对中国不公平的贸易政策表示担忧。双方一致认为应以慎重负责的态度发展军事领域的人工智能技术,应维持由人类控制核武器使用的决定。美国总统 Biden 将于2025年1月正式卸任。摄影师:Leah Millis", + "coordinate_sets": [], + "entry_imgs": [], + "tags": [ + { + "id": "spotnews", + "name": "SPOT NEWS · 全球焦点", + "focus": 1 + } + ], + "news_count": 7 + } +] diff --git a/tests/commands/test_idaily/test_idaily.py b/tests/commands/test_idaily/test_idaily.py new file mode 100644 index 00000000..0f3a004a --- /dev/null +++ b/tests/commands/test_idaily/test_idaily.py @@ -0,0 +1,26 @@ +import json +import unittest + +from wechatter.commands._commands import idaily + + +class TestIdailyCommand(unittest.TestCase): + def setUp(self): + with open( + "tests/commands/test_idaily/idaily_response.json" + ) as f: + self.tih_response = json.load(f) + self.tih_list = self.tih_response + + def test_extract_idaily_data_success(self): + result = idaily._extract_idaily_data(self.tih_response) + self.assertListEqual(result, self.tih_list) + + def test_generate_idaily_message_success(self): + result = idaily._generate_idaily_message(self.tih_list) + true_result = "✨====每日环球视野====✨\n今天的iDaily还没更新,现在为您呈现的是:\n🗓️ 时间: November 17, 2024\n1. 🌎 泰国庆祝天灯节\n 🌪️ 泰国民众放飞孔明灯庆祝「天灯节」(Yi Peng festival),清迈。「天灯节」是泰国北部地区的传统节日,历史可追溯至13世纪泰北兰纳王国时期,庆祝日期为每年泰国农历第12个月的满月日,人们会放飞孔明灯庆祝新一年即将开始。泰国旅游部数据显示2024年1至10月接待外国游客超过2900万人次,创造1.35万亿泰铢(约合393亿美元)旅游业收入,前5大游客来源国依次为中国、马来西亚、印度、韩国和俄罗斯。摄影师:Manan Vatsyayana\n2. 🌎 中美元首利马会晤\n 🌪️ 中国国家主席习近平与美国总统 Joe Biden 在 APEC 峰会期间举行会晤,秘鲁利马。11月16日,中美两国领导人就双边关系、人工智能治理、地区及国际局势议题举行1小时45分钟会谈。习近平就台湾问题、中国南海、经贸科技、网络安全、乌克兰危机、朝鲜半岛局势等重大问题阐明中方立场。Biden 强调美国的一个中国政策保持不变,对中国支持俄罗斯国防工业深表关切,对中国不公平的贸易政策表示担忧。双方一致认为应以慎重负责的态度发展军事领域的人工智能技术,应维持由人类控制核武器使用的决定。美国总统 Biden 将于2025年1月正式卸任。摄影师:Leah Millis" + self.assertIn(true_result, result) + + def test_generate_idaily_message_empty_list(self): + result = idaily._generate_idaily_message([]) + self.assertEqual(result, "暂无每日环球视野") diff --git a/tests/commands/test_today_in_history/test_today_in_history.py b/tests/commands/test_today_in_history/test_today_in_history.py deleted file mode 100644 index 2e1efde9..00000000 --- a/tests/commands/test_today_in_history/test_today_in_history.py +++ /dev/null @@ -1,30 +0,0 @@ -import json -import unittest - -from wechatter.commands._commands import today_in_history - - -class TestTodayInHistoryCommand(unittest.TestCase): - def setUp(self): - with open( - "tests/commands/test_today_in_history/today_in_history_response.json" - ) as f: - self.tih_response = json.load(f) - self.tih_list = self.tih_response["data"] - - def test_extract_today_in_history_data_success(self): - result = today_in_history._extract_today_in_history_data(self.tih_response) - self.assertListEqual(result, self.tih_list) - - def test_extract_today_in_history_data_failure(self): - with self.assertRaises(RuntimeError): - today_in_history._extract_today_in_history_data({}) - - def test_generate_today_in_history_message_success(self): - result = today_in_history._generate_today_in_history_message(self.tih_list) - true_result = "1. 🗓️ 1421 年\n 🌎 明朝正式迁都北京\n 🌪️ 明朝(1368-1644年)是中国历史上最后一个由汉族建立的中原王朝。历经十二世、十六位皇帝,国祚二百七十六年。1368年明太祖朱元璋在南京应天府称帝,国号大明。\n2. 🗓️ 1895 年\n 🌎 二·二八事件受难者陈澄波出生\n 🌪️ 陈澄波(1895-1947),生于嘉义,一九一三年考进台湾总督府国语学校师范科(今台北师范学校),在校期间获石川钦—郎指导对西洋\n3. 🗓️ 1901 年\n 🌎 立陶宛小提琴家亚莎·海菲兹出生\n 🌪️ 亚莎·海菲兹(Jascha·Heifetz,1901-1987),二十世纪杰出的美籍立陶宛小提琴家;1901年出生于当时属于俄罗斯的立陶宛首都维尔\n4. 🗓️ 1919 年\n 🌎 葡萄牙宣布成立君主国\n 🌪️ 葡萄牙,全称葡萄牙共和国(英语:The Portuguese Republic,葡萄牙语:República Portuguesa),是一个位于欧洲西南部的共和制\n5. 🗓️ 1920 年\n 🌎 北洋政府教育部发布中国第一套法定的新式标点符号\n 🌪️ 1920年4月,胡适、钱玄同、刘复、朱希祖、周作人、马裕藻六位教授首次提出《请颁行新式标点符号方案》,方案次年被批准,成为语言文化发展史上的重要里程碑。\n6. 🗓️ 1925 年\n 🌎 军事理论家米哈伊尔·伏龙芝逝世\n 🌪️ 苏联红军统帅,军事理论家。开创了将革命激情和现代化武装相结合的道路。生于谢米列奇耶州皮什佩克城一医士家庭。1917年十月革\n7. 🗓️ 1937 年\n 🌎 东北军将领王以哲逝世\n 🌪️ 王以哲(1896—1937)字鼎芳,汉族,原名王海山,东北军重要将领之一。国民革命军陆军中将。1896年出生于宾州厅东偏脸子屯(今\n8. 🗓️ 1943 年\n 🌎 斯大林格勒会战结束\n 🌪️ 斯大林格勒战役(俄语:Сталинградская битва,德语:Schlacht von Stalingrad)是第二次世界大战\n9. 🗓️ 1948 年\n 🌎 台湾漫画家蔡志忠出生\n 🌪️ 蔡志忠,生于1948年,台湾彰化人,著名漫画家。15岁起便开始成为职业漫画家,1971年底进入光启社任美术设计,并自学卡通绘制技\n10. 🗓️ 1960 年\n 🌎 香港著名演员惠英红出生\n 🌪️ 惠英红,1960年2月2日生于香港,祖籍山东,满洲正黄旗人,香港著名女演员。香港电影金像奖影后。打女代表人物之一。1977年凭张\n11. 🗓️ 1970 年\n 🌎 英国哲学家伯特兰·罗素逝世\n 🌪️ 伯特兰·罗素(Bertrand Russell,1872—1970)是二十世纪英国哲学家、数学家、逻辑学家、历史学家,无神论或者不可知论者,也是\n12. 🗓️ 1977 年\n 🌎 世界杯主题曲《Wakawaka》演唱者夏奇拉出生\n 🌪️ 夏奇拉(Shakira),1977年2月2日出生于哥伦比亚巴兰基亚,哥伦比亚歌手。1991年,夏奇拉发行了个人首张专辑《Magia》。1998年\n13. 🗓️ 1990 年\n 🌎 南非总统德克勒克宣布废除南非种族隔离制度\n 🌪️ 南非政治家,南非共和国白人总统(1936-),他结束了南非种族隔离制度。并议定了向多数派执政的过渡,因此和纳尔逊·曼德拉 一起" - self.assertIn(true_result, result) - - def test_generate_today_in_history_message_empty_list(self): - result = today_in_history._generate_today_in_history_message([]) - self.assertEqual(result, "暂无历史上的今天") diff --git a/tests/commands/test_today_in_history/today_in_history_response.json b/tests/commands/test_today_in_history/today_in_history_response.json deleted file mode 100644 index fae3f4b3..00000000 --- a/tests/commands/test_today_in_history/today_in_history_response.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "status": 200, - "message": "OK: 6:16:39 PM GMT+8", - "data": [ - { - "year": "1421", - "title": "明朝正式迁都北京", - "desc": "明朝(1368-1644年)是中国历史上最后一个由汉族建立的中原王朝。历经十二世、十六位皇帝,国祚二百七十六年。1368年明太祖朱元璋在南京应天府称帝,国号大明。", - "link": "https://baike.baidu.com/item/%E6%98%8E%E6%9C%9D/141291" - }, - { - "year": "1895", - "title": "二·二八事件受难者陈澄波出生", - "desc": "陈澄波(1895-1947),生于嘉义,一九一三年考进台湾总督府国语学校师范科(今台北师范学校),在校期间获石川钦—郎指导对西洋", - "link": "https://baike.baidu.com/item/%E9%99%88%E6%BE%84%E6%B3%A2/2823558" - }, - { - "year": "1901", - "title": "立陶宛小提琴家亚莎·海菲兹出生", - "desc": "亚莎·海菲兹(Jascha·Heifetz,1901-1987),二十世纪杰出的美籍立陶宛小提琴家;1901年出生于当时属于俄罗斯的立陶宛首都维尔", - "link": "https://baike.baidu.com/item/%E4%BA%9A%E8%8E%8E%C2%B7%E6%B5%B7%E8%8F%B2%E5%85%B9" - }, - { - "year": "1919", - "title": "葡萄牙宣布成立君主国", - "desc": "葡萄牙,全称葡萄牙共和国(英语:The Portuguese Republic,葡萄牙语:República Portuguesa),是一个位于欧洲西南部的共和制", - "link": "https://baike.baidu.com/item/%E8%91%A1%E8%90%84%E7%89%99" - }, - { - "year": "1920", - "title": "北洋政府教育部发布中国第一套法定的新式标点符号", - "desc": "1920年4月,胡适、钱玄同、刘复、朱希祖、周作人、马裕藻六位教授首次提出《请颁行新式标点符号方案》,方案次年被批准,成为语言文化发展史上的重要里程碑。", - "link": "https://baike.baidu.com/item/%E6%A0%87%E7%82%B9%E7%AC%A6%E5%8F%B7" - }, - { - "year": "1925", - "title": "军事理论家米哈伊尔·伏龙芝逝世", - "desc": "苏联红军统帅,军事理论家。开创了将革命激情和现代化武装相结合的道路。生于谢米列奇耶州皮什佩克城一医士家庭。1917年十月革", - "link": "https://baike.baidu.com/item/%E7%B1%B3%E5%93%88%E4%BC%8A%E5%B0%94%C2%B7%E7%93%A6%E8%A5%BF%E9%87%8C%E8%80%B6%E7%BB%B4%E5%A5%87%C2%B7%E4%BC%8F%E9%BE%99%E8%8A%9D" - }, - { - "year": "1937", - "title": "东北军将领王以哲逝世", - "desc": "王以哲(1896—1937)字鼎芳,汉族,原名王海山,东北军重要将领之一。国民革命军陆军中将。1896年出生于宾州厅东偏脸子屯(今", - "link": "https://baike.baidu.com/item/%E7%8E%8B%E4%BB%A5%E5%93%B2" - }, - { - "year": "1943", - "title": "斯大林格勒会战结束", - "desc": "斯大林格勒战役(俄语:Сталинградская битва,德语:Schlacht von Stalingrad)是第二次世界大战", - "link": "https://baike.baidu.com/item/%E6%96%AF%E5%A4%A7%E6%9E%97%E6%A0%BC%E5%8B%92%E4%BC%9A%E6%88%98" - }, - { - "year": "1948", - "title": "台湾漫画家蔡志忠出生", - "desc": "蔡志忠,生于1948年,台湾彰化人,著名漫画家。15岁起便开始成为职业漫画家,1971年底进入光启社任美术设计,并自学卡通绘制技", - "link": "https://baike.baidu.com/item/%E8%94%A1%E5%BF%97%E5%BF%A0/3403" - }, - { - "year": "1960", - "title": "香港著名演员惠英红出生", - "desc": "惠英红,1960年2月2日生于香港,祖籍山东,满洲正黄旗人,香港著名女演员。香港电影金像奖影后。打女代表人物之一。1977年凭张", - "link": "https://baike.baidu.com/item/%E6%83%A0%E8%8B%B1%E7%BA%A2" - }, - { - "year": "1970", - "title": "英国哲学家伯特兰·罗素逝世", - "desc": "伯特兰·罗素(Bertrand Russell,1872—1970)是二十世纪英国哲学家、数学家、逻辑学家、历史学家,无神论或者不可知论者,也是", - "link": "https://baike.baidu.com/item/%E4%BC%AF%E7%89%B9%E5%85%B0%C2%B7%E7%BD%97%E7%B4%A0" - }, - { - "year": "1977", - "title": "世界杯主题曲《Wakawaka》演唱者夏奇拉出生", - "desc": "夏奇拉(Shakira),1977年2月2日出生于哥伦比亚巴兰基亚,哥伦比亚歌手。1991年,夏奇拉发行了个人首张专辑《Magia》。1998年", - "link": "https://baike.baidu.com/item/%E5%A4%8F%E5%A5%87%E6%8B%89" - }, - { - "year": "1990", - "title": "南非总统德克勒克宣布废除南非种族隔离制度", - "desc": "南非政治家,南非共和国白人总统(1936-),他结束了南非种族隔离制度。并议定了向多数派执政的过渡,因此和纳尔逊·曼德拉 一起", - "link": "https://baike.baidu.com/item/%E5%BC%97%E9%9B%B7%E5%BE%B7%E9%87%8C%E5%85%8B%C2%B7%E5%A8%81%E5%BB%89%C2%B7%E5%BE%B7%E5%85%8B%E5%8B%92%E5%85%8B" - } - ] -} diff --git a/wechatter/commands/_commands/idaily.py b/wechatter/commands/_commands/idaily.py new file mode 100644 index 00000000..30686017 --- /dev/null +++ b/wechatter/commands/_commands/idaily.py @@ -0,0 +1,108 @@ +from typing import Dict, Union + +from loguru import logger + +from wechatter.commands.handlers import command +from wechatter.models.wechat import SendTo +from wechatter.sender import sender +from wechatter.utils import get_request_json +from wechatter.utils.time import get_current_bdy, get_yesterday_bdy + + +@command( + command="idaily", + keys=["每日环球视野", "idaily"], + desc="获取每日环球视野。", +) +def idaily_command_handler(to: Union[str, SendTo], message: str = "") -> None: + # 获取每日环球视野 + try: + result = get_idaily_str() + except Exception as e: + error_message = f"获取每日环球视野失败,错误信息:{str(e)}" + logger.error(error_message) + sender.send_msg(to, error_message) + else: + sender.send_msg(to, result) + + +@idaily_command_handler.mainfunc +def get_idaily_str() -> str: + response = get_request_json(url="https://idaily-cdn.idailycdn.com/api/list/v3/iphone") + tih_list = _extract_idaily_data(response) + return _generate_idaily_message(tih_list) + + +def _extract_idaily_data(r_json: Dict) -> dict: + try: + tih_list = r_json + except (KeyError, TypeError) as e: + logger.error("解析每日环球视野API返回的JSON失败") + raise RuntimeError("解析每日环球视野API返回的JSON失败") from e + return tih_list + + +def _generate_idaily_message(tih_list: dict) -> str: + if not tih_list: + return "暂无每日环球视野" + + idaily_str = ["✨====每日环球视野====✨"] + content_list = [] + today = get_current_bdy() + yesterday = get_yesterday_bdy() + today_has_idaily = False + + def format_entry(index, entry): + title = entry['title_wechat_tml'].split(" - ")[0] + content = entry['content'] + return f"{index + 1}. 🌎 {title}\n 🌪️ {content}" + + for index, entry in enumerate(tih_list): + if entry["pubdate"] == str(today): + today_has_idaily = True + content_list.append(format_entry(index, entry)) + elif not today_has_idaily and entry["pubdate"] == str(yesterday): + content_list.append(format_entry(index, entry)) + + if today_has_idaily: + idaily_str.append(f"🗓️ 今天是 {today}") + else: + idaily_str.append("今天的iDaily还没更新,现在为您呈现的是:") + idaily_str.append(f"🗓️ 时间: {yesterday}") + + idaily_str.extend(content_list) + return "\n".join(idaily_str) + + +# def _generate_idaily_message(tih_list: dict) -> str: +# if not tih_list: +# return "暂无每日环球视野" +# +# idaily_str = "✨=====每日环球视野=====✨\n" +# this_str = "" +# _today = get_current_bdy() +# today_has_idaily = False +# +# _yesterday = get_yesterday_bdy() +# for i in range(len(tih_list)): +# if tih_list[i]["pubdate"] == str(_today): +# today_has_idaily = True +# title_wechat_tml = tih_list[i]['title_wechat_tml'].split(" - ")[0] +# this_str += ( +# f"{i + 1}. 🌎 {title_wechat_tml}\n" +# f" 🌪️ {tih_list[i]['content']}\n" +# ) +# if not today_has_idaily: +# if tih_list[i]["pubdate"] == str(_yesterday): +# title_wechat_tml = tih_list[i]['title_wechat_tml'].split(" - ")[0] +# this_str += ( +# f"{i + 1}. 🌎 {title_wechat_tml}\n" +# f" 🌪️ {tih_list[i]['content']}\n" +# ) +# if today_has_idaily: +# idaily_str += "🗓️ 今天是" + _today + "\n" +# else: +# idaily_str += "今天的iDaily还没更新,现在为您呈现的是:\n" +# idaily_str += "🗓️ 时间: " + _yesterday + "\n" +# idaily_str += this_str +# return idaily_str diff --git a/wechatter/commands/_commands/spark_chat.py b/wechatter/commands/_commands/spark_chat.py new file mode 100644 index 00000000..a21d2ecf --- /dev/null +++ b/wechatter/commands/_commands/spark_chat.py @@ -0,0 +1,490 @@ +from datetime import datetime +from typing import Union, List + +from loguru import logger + +from wechatter.commands.handlers import command +from wechatter.config import config +from wechatter.database import ( + GptChatInfo as DbGptChatInfo, + GptChatMessage as DbGptChatMessage, + make_db_session, +) +from wechatter.models.gpt import GptChatInfo +from wechatter.models.wechat import Person, SendTo +from wechatter.sender import sender +from wechatter.utils import post_request_json +from wechatter.utils.time import get_current_date, get_current_week, get_current_time + +DEFAULT_TOPIC = "(对话进行中*)" +# TODO: 初始化对话,Prompt选择 +DEFAULT_CONVERSATION = [ + { + "role": "system", + "content": f"你的名字是 WeChatter,是一位虚拟助手。今天是{get_current_date()}(年月日),星期{get_current_week()},现在是{get_current_time()}。", + } +] + +this_model = config["spark_model"] + + +@command( + command="spark", + keys=["spark", "spark_chat"], + desc="与 Spark AI 聊天", +) +def spark_command_handler(to: SendTo, message: str = "", message_obj=None) -> None: + _gptx(this_model, to, message, message_obj) + + +@command( + command="spark-chats", + keys=["spark-chats", "spark对话记录"], + desc="列出Spark对话记录。", +) +def spark_chats_command_handler(to: SendTo, message: str = "", message_obj=None) -> None: + _gptx_chats(this_model, to, message, message_obj) + + +@command( + command="spark-record", + keys=["spark-record", "spark记录"], + desc="获取Spark对话记录。", +) +def spark_record_command_handler( + to: SendTo, message: str = "", message_obj=None +) -> None: + _gptx_record(this_model, to, message) + + +@command( + command="spark-continue", + keys=["spark-continue", "spark继续"], + desc="继续Spark对话。", +) +def spark_continue_command_handler( + to: SendTo, message: str = "", message_obj=None +) -> None: + _gptx_continue(this_model, to, message) + + +def _gptx(model: str, to: SendTo, message: str = "", message_obj=None) -> None: + person = to.person + # 获取文件夹下最新的对话记录 + chat_info = SparkChat.get_chatting_chat_info(person, model) + if message == "": # /gpt4 + # 判断对话是否有效 + sender.send_msg(to, "正在创建新对话...") + if chat_info is None or SparkChat._is_chat_valid(chat_info): + SparkChat.create_chat(person, model) + logger.info("创建新对话成功") + sender.send_msg(to, "创建新对话成功") + return + logger.info("对话未开始,继续上一次对话") + sender.send_msg(to, "对话未开始,继续上一次对话") + else: # /gpt4 + # 如果没有对话记录,则创建新对话 + sender.send_msg(to, f"正在调用 {model} 进行对话...") + if chat_info is None: + chat_info = SparkChat.create_chat(person, model) + logger.info("无历史对话记录,创建新对话成功") + sender.send_msg(to, "无历史对话记录,创建新对话成功") + try: + response = SparkChat.chat( + chat_info, message=message, message_obj=message_obj + ) + logger.info(response) + sender.send_msg(to, response) + except Exception as e: + error_message = f"调用{this_model}服务失败,错误信息:{str(e)}" + logger.error(error_message) + sender.send_msg(to, error_message) + + +def _gptx_chats(model: str, to: SendTo, message: str = "", message_obj=None) -> None: + response = SparkChat.get_chat_list_str(to.person, model) + sender.send_msg(to, response) + + +def _gptx_record(model: str, to: SendTo, message: str = ""): + person = to.person + if message == "": + # 获取当前对话的对话记录 + chat_info = SparkChat.get_chatting_chat_info(person, model) + else: + # 获取指定对话的对话记录 + chat_info = SparkChat.get_chat_info(person, model, int(message)) + if chat_info is None: + logger.warning("对话不存在") + sender.send_msg(to, "对话不存在") + return + response = SparkChat.get_brief_conversation_str(chat_info) + logger.info(response) + sender.send_msg(to, response) + + +def _gptx_continue(model: str, to: SendTo, message: str = "") -> None: + person = to.person + # 判断message是否为数字 + if not message.isdigit(): + logger.info("请输入对话记录编号") + sender.send_msg(to, "请输入对话记录编号") + return + sender.send_msg(to, f"正在切换到对话记录 {message}...") + chat_info = SparkChat.continue_chat( + person=person, model=model, chat_index=int(message) + ) + if chat_info is None: + warning_message = "选择历史对话失败,对话不存在" + logger.warning(warning_message) + sender.send_msg(to, warning_message) + return + response = SparkChat.get_brief_conversation_str(chat_info) + response += "====================\n" + response += "对话已选中,输入命令继续对话" + logger.info(response) + sender.send_msg(to, response) + + +class SparkChat: + spark_api = config["spark_api"] + token = "Bearer " + config["spark_token"] + + @staticmethod + def create_chat(person: Person, model: str) -> GptChatInfo: + """ + 创建一个新的对话 + :param person: 用户 + :param model: 模型 + :return: 新的对话信息 + """ + # 生成上一次对话的主题 + SparkChat._save_chatting_chat_topic(person, model) + SparkChat._set_all_chats_not_chatting(person, model) + gpt_chat_info = GptChatInfo( + person=person, + model=model, + topic=DEFAULT_TOPIC, + is_chatting=True, + ) + with make_db_session() as session: + _gpt_chat_info = DbGptChatInfo.from_model(gpt_chat_info) + session.add(_gpt_chat_info) + session.commit() + # 获取 SQLite 自动生成的 chat_id + session.refresh(_gpt_chat_info) + gpt_chat_info = _gpt_chat_info.to_model() + return gpt_chat_info + + @staticmethod + def continue_chat( + person: Person, model: str, chat_index: int + ) -> Union[GptChatInfo, None]: + """ + 继续对话,选择历史对话 + :param person: 用户 + :param model: 模型 + :param chat_index: 对话记录索引(从1开始) + :return: 对话信息 + """ + # 读取对话记录文件 + chat_info = SparkChat.get_chat_info(person, model, chat_index) + if chat_info is None: + return None + chatting_chat_info = SparkChat.get_chatting_chat_info(person, model) + if chatting_chat_info: + if not SparkChat._is_chat_valid(chatting_chat_info): + # 如果对话无效,则删除该对话记录后再继续对话 + SparkChat._delete_chat(chatting_chat_info) + else: + # 生成上一次对话的主题 + SparkChat._save_chatting_chat_topic(person, model) + SparkChat._set_chatting_chat(person, model, chat_info) + return chat_info + + @staticmethod + def _set_chatting_chat(person: Person, model: str, chat_info: GptChatInfo) -> None: + """ + 设置正在进行中的对话记录 + """ + # 先将所有对话记录的 is_chatting 字段设置为 False + SparkChat._set_all_chats_not_chatting(person, model) + with make_db_session() as session: + chat_info = session.query(DbGptChatInfo).filter_by(id=chat_info.id).first() + if chat_info is None: + logger.error("对话记录不存在") + raise ValueError("对话记录不存在") + chat_info.is_chatting = True + session.commit() + + @staticmethod + def _delete_chat(chat_info: GptChatInfo) -> None: + """ + 删除对话记录 + """ + with make_db_session() as session: + session.query(DbGptChatMessage).filter_by(gpt_chat_id=chat_info.id).delete() + session.query(DbGptChatInfo).filter_by(id=chat_info.id).delete() + session.commit() + + @staticmethod + def get_brief_conversation_str(chat_info: GptChatInfo) -> str: + """ + 获取对话记录的字符串 + :param chat_info: 对话记录 + :return: 对话记录字符串 + """ + with make_db_session() as session: + chat_info = session.query(DbGptChatInfo).filter_by(id=chat_info.id).first() + if chat_info is None: + logger.error("对话记录不存在") + raise ValueError("对话记录不存在") + conversation_str = f"✨==={chat_info.topic}===✨\n" + if not chat_info.gpt_chat_messages: + conversation_str += " 无对话记录" + return conversation_str + for msg in chat_info.gpt_chat_messages: + content: str = msg.message.content + # 合并成一行,提升观感 + content = content.replace("\n", "") + # 去掉命令前缀和命令关键词 + content = content[content.find(" ") + 1:][:30] + response = msg.gpt_response[:30] + response = response.replace("\n", "") + if len(msg.message.content) > 30: + content += "..." + if len(msg.gpt_response) > 30: + response += "..." + conversation_str += f"💬:{content}\n" + conversation_str += f"🤖:{response}\n" + return conversation_str + + @staticmethod + def _set_all_chats_not_chatting(person: Person, model: str) -> None: + """ + 将所有对话记录的 is_chatting 字段设置为 False + """ + with make_db_session() as session: + session.query(DbGptChatInfo).filter_by( + person_id=person.id, model=model + ).update({"is_chatting": False}) + session.commit() + + @staticmethod + def _list_chat_info(person: Person, model: str) -> List: + """ + 列出用户的所有对话记录 + """ + # 按照 chat_talk_time 字段倒序排序,取前20个 + with make_db_session() as session: + chat_info_list = ( + session.query(DbGptChatInfo) + .filter_by(person_id=person.id, model=model) + .order_by( + DbGptChatInfo.is_chatting.desc(), + DbGptChatInfo.talk_time.desc(), + ) + .limit(20) + .all() + ) + _chat_info_list = [] + for chat_info in chat_info_list: + _chat_info_list.append(chat_info.to_model()) + return _chat_info_list + + @staticmethod + def get_chat_list_str(person: Person, model: str) -> str: + """ + 获取用户的所有对话记录 + :param person: 用户 + :param model: 模型 + :return: 对话记录 + """ + chat_info_list = SparkChat._list_chat_info(person, model) + chat_info_list_str = f"✨==={model}对话记录===✨\n" + if not chat_info_list: + chat_info_list_str += " 📭 无对话记录" + return chat_info_list_str + with make_db_session() as session: + for i, chat_info in enumerate(chat_info_list): + chat = session.query(DbGptChatInfo).filter_by(id=chat_info.id).first() + if chat.is_chatting: + chat_info_list_str += f"{i + 1}. 💬{chat.topic}\n" + else: + chat_info_list_str += f"{i + 1}. {chat.topic}\n" + return chat_info_list_str + + @staticmethod + def get_chat_info( + person: Person, model: str, chat_index: int + ) -> Union[GptChatInfo, None]: + """ + 获取用户的对话信息 + :param person: 用户 + :param model: 模型 + :param chat_index: 对话记录索引(从1开始) + :return: 对话信息 + """ + chat_info_id_list = SparkChat._list_chat_info(person, model) + if not chat_info_id_list: + return None + if chat_index <= 0 or chat_index > len(chat_info_id_list): + return None + return chat_info_id_list[chat_index - 1] + + @staticmethod + def get_chatting_chat_info(person: Person, model: str) -> Union[GptChatInfo, None]: + """ + 获取正在进行中的对话信息 + :param person: 用户 + :param model: 模型 + :return: 对话信息 + """ + with make_db_session() as session: + chat_info = ( + session.query(DbGptChatInfo) + .filter_by(person_id=person.id, model=model, is_chatting=True) + .first() + ) + if not chat_info: + return None + return chat_info.to_model() + + @staticmethod + def chat(chat_info: GptChatInfo, message: str, message_obj) -> str: + """ + 持续对话 + :param chat_info: 对话信息 + :param message: 用户消息 + :param message_obj: 消息对象 + :return: GPT 回复 + """ + # 对外暴露的对话方法,必须保存对话记录 + return SparkChat._chat( + chat_info=chat_info, message=message, message_obj=message_obj, is_save=True + ) + + @staticmethod + def _chat(chat_info: GptChatInfo, message: str, message_obj, is_save: bool) -> str: + """ + 持续对话 + :param chat_info: 对话信息 + :param message: 用户消息 + :param message_obj: 消息对象 + :param is_save: 是否保存此轮对话记录 + :return: GPT 回复 + """ + newconv = [{"role": "user", "content": message}] + # 发送请求 + headers = { + "Authorization": SparkChat.token, + "Content-Type": "application/json", + } + json = { + "model": this_model, + "messages": DEFAULT_CONVERSATION + chat_info.get_conversation() + newconv, + } + r_json = post_request_json( + url=SparkChat.spark_api, headers=headers, json=json, timeout=60 + ) + + print(r_json) + # 判断是否有 error 或 code 字段 + if r_json: + SparkChat._check_r_json(r_json) + + msg = r_json["choices"][0]["message"] + msg_content = msg.get("content", "调用" + this_model + "服务失败") + # 将返回的 assistant 回复添加到对话记录中 + if is_save is True: + newconv.append({"role": "assistant", "content": msg_content}) + chat_info.extend_conversation(newconv) + with make_db_session() as session: + _chat_info = ( + session.query(DbGptChatInfo).filter_by(id=chat_info.id).first() + ) + _chat_info.talk_time = datetime.now() + for chat_message in chat_info.gpt_chat_messages[-len(newconv) // 2:]: + _chat_message = DbGptChatMessage.from_model(chat_message) + _chat_message.message_id = message_obj.id + _chat_info.gpt_chat_messages.append(_chat_message) + session.commit() + return msg_content + + @staticmethod + def _check_r_json(r_json): + if "error" in r_json: + raise ValueError(this_model + " 服务返回值错误") + if "code" in r_json: + code = r_json["code"] + error_messages = { + 0: None, # 成功,不抛出异常 + 10007: this_model + " 用户流量受限:服务正在处理用户当前的问题,需等待处理完成后再发送新的请求。(必须要等大模型完全回复之后,才能发送下一个问题)", + 10013: this_model + " 输入内容审核不通过,涉嫌违规,请重新调整输入内容", + 10014: this_model + " 输出内容涉及敏感信息,审核不通过,后续结果无法展示给用户", + 10019: this_model + " 表示本次会话内容有涉及违规信息的倾向;建议开发者收到此错误码后给用户一个输入涉及违规的提示", + 10907: this_model + " token数量超过上限。对话历史+问题的字数太多,需要精简输入", + 11200: this_model + " 授权错误:该appId没有相关功能的授权 或者 业务量超过限制", + 11201: this_model + " 授权错误:日流控超限。超过当日最大访问量的限制", + 11202: this_model + " 授权错误:秒级流控超限。秒级并发超过授权路数限制", + 11203: this_model + " 授权错误:并发流控超限。并发路数超过授权路数限制", + } + error_message = error_messages.get(code) + if error_message: + raise ValueError(error_message) + + @staticmethod + def _save_chatting_chat_topic(person: Person, model: str) -> None: + """ + 生成正在进行的对话的主题 + """ + chat_info = SparkChat.get_chatting_chat_info(person, model) + if chat_info is None or SparkChat._has_topic(chat_info): + return + # 生成对话主题 + if not SparkChat._is_chat_valid(chat_info): + logger.error("对话记录长度小于1") + return + + topic = SparkChat._generate_chat_topic(chat_info) + if not topic: + logger.error("生成对话主题失败") + raise ValueError("生成对话主题失败") + # 更新对话主题 + with make_db_session() as session: + chat_info = session.query(DbGptChatInfo).filter_by(id=chat_info.id).first() + chat_info.topic = topic + session.commit() + + @staticmethod + def _generate_chat_topic(chat_info: GptChatInfo) -> str: + """ + 生成对话主题,用于保存对话记录 + """ + assert SparkChat._is_chat_valid(chat_info) + # 通过一次对话生成对话主题,但这次对话不保存到对话记录中 + prompt = "请用10个字以内总结一下这次对话的主题,不带任何标点符号" + topic = SparkChat._chat( + chat_info=chat_info, message=prompt, message_obj=None, is_save=False + ) + # 限制主题长度 + if len(topic) > 21: + topic = topic[:21] + "..." + logger.info(f"生成对话主题:{topic}") + return topic + + @staticmethod + def _has_topic(chat_info: GptChatInfo) -> bool: + """ + 判断对话是否有主题 + """ + return chat_info.topic != DEFAULT_TOPIC + + @staticmethod + def _is_chat_valid(chat_info: GptChatInfo) -> bool: + """ + 判断对话是否有效 + """ + if chat_info.gpt_chat_messages: + return True + return False diff --git a/wechatter/commands/_commands/today_in_history.py b/wechatter/commands/_commands/today_in_history.py deleted file mode 100644 index e8611b9d..00000000 --- a/wechatter/commands/_commands/today_in_history.py +++ /dev/null @@ -1,56 +0,0 @@ -from typing import Dict, List, Union - -from loguru import logger - -from wechatter.commands.handlers import command -from wechatter.models.wechat import SendTo -from wechatter.sender import sender -from wechatter.utils import get_request_json - - -@command( - command="today-in-history", - keys=["历史上的今天", "today-in-history", "t-i-h"], - desc="获取历史上的今天。", -) -def today_in_history_command_handler(to: Union[str, SendTo], message: str = "") -> None: - # 获取历史上的今天 - try: - result = get_today_in_history_str() - except Exception as e: - error_message = f"获取历史上的今天失败,错误信息:{str(e)}" - logger.error(error_message) - sender.send_msg(to, error_message) - else: - sender.send_msg(to, result) - - -@today_in_history_command_handler.mainfunc -def get_today_in_history_str() -> str: - response = get_request_json(url="https://60s-view.deno.dev/history") - tih_list = _extract_today_in_history_data(response) - return _generate_today_in_history_message(tih_list) - - -def _extract_today_in_history_data(r_json: Dict) -> List: - try: - tih_list = r_json["data"] - except (KeyError, TypeError) as e: - logger.error("解析历史上的今天API返回的JSON失败") - raise RuntimeError("解析历史上的今天API返回的JSON失败") from e - return tih_list - - -def _generate_today_in_history_message(tih_list: List) -> str: - if not tih_list: - return "暂无历史上的今天" - - today_in_history_str = "✨=====历史上的今天=====✨\n" - for i, today_in_history in enumerate(tih_list): - today_in_history_str += ( - f"{i + 1}. 🗓️ {today_in_history.get('year')} 年\n" - f" 🌎 {today_in_history.get('title')}\n" - f" 🌪️ {today_in_history.get('desc')}\n" - ) - - return today_in_history_str diff --git a/wechatter/commands/_commands/trivia.py b/wechatter/commands/_commands/trivia.py index 8bd613fd..2cb27861 100644 --- a/wechatter/commands/_commands/trivia.py +++ b/wechatter/commands/_commands/trivia.py @@ -19,10 +19,10 @@ desc="获取冷知识。", ) def trivia_command_handler(to: Union[str, SendTo], message: str = "") -> None: - random_number = random.randint(1, 917) # nosec + random_number = random.randint(1, 946) # nosec try: response = get_request( - url=f"http://www.quzhishi.com/shiwangelengzhishi/{random_number}.html" + url=f"http://www.zhangzaixi.com/shiwangelengzhishi/{random_number}.html" ) trivia_list = _parse_trivia_response(response) result = _generate_trivia_message(trivia_list, random_number) diff --git a/wechatter/models/wechat/message.py b/wechatter/models/wechat/message.py index 5af50fdc..fae25086 100644 --- a/wechatter/models/wechat/message.py +++ b/wechatter/models/wechat/message.py @@ -110,15 +110,20 @@ def from_api_msg( _group = None # room为群信息,只有群消息才有room - if source_json["room"] != {}: - g_data = source_json["room"] - payload = g_data.get("payload", {}) - _group = Group( - id=g_data.get("id", ""), - name=payload.get("topic", ""), - admin_id_list=payload.get("adminIdList", []), - member_list=payload.get("memberList", []), - ) + if source_json["room"] != '': + if "room" in source_json and isinstance(source_json["room"], dict): + g_data = source_json["room"] + payload = g_data.get("payload", {}) + _group = Group( + id=g_data.get("id", ""), + name=payload.get("topic", ""), + admin_id_list=payload.get("adminIdList", []), + member_list=payload.get("memberList", []), + ) + else: + logger.error("source_json[room]: " + str(source_json["room"])) + # else: + # logger.warning("source_json[room]是空的,不是群信息") _receiver = None if source_json.get("to"): diff --git a/wechatter/utils/time.py b/wechatter/utils/time.py index 6f7d62f5..e5f8b2f6 100644 --- a/wechatter/utils/time.py +++ b/wechatter/utils/time.py @@ -1,6 +1,6 @@ # 获取时间工具类 import time -from datetime import datetime +from datetime import datetime, timedelta def get_current_hour() -> int: @@ -105,3 +105,20 @@ def get_current_ymdh() -> str: :return: 返回格式化后的年月日时字符串 """ return time.strftime("%Y%m%d%H", time.localtime()) + + +def get_current_bdy() -> str: + """ + 获取当前月(英文)日年 + :return: 返回格式化后的月(英文)日年字符串,输出: November 16, 2024 + """ + return time.strftime("%B %d, %Y", time.localtime()) + + +def get_yesterday_bdy() -> str: + """ + 获取昨天月(英文)日年 + :return: 返回格式化后的昨天月(英文)日年字符串,输出: November 16, 2024 + """ + yesterday = datetime.now() - timedelta(days=1) + return yesterday.strftime("%B %d, %Y")