TG Bot开发之创意篇---第二章:实现天气预报

405 阅读6分钟

创意篇---第二章:实现天气预报

天气预报的实现功能本质上等同于接入ai搜索引擎,都是通过api发送请求,因此如果你不了解如何利用api发送请求,请移步至之前的教程

获取和风天气的ApiKey

首先打开和风天气开发文档 项目和KEY | 和风天气开发服务 (qweather.com)

image.png

根据要求,需要先创建项目,在这里我创建项目时选择的是免费订阅,即每天有1k次免费请求的次数

image.png

接着,在开发文档中选择自己要请求的服务,这里请求实时天气预报为例,由于是免费订阅,请求地址格式为:

https://devapi.qweather.com/v7/weather/now?location=101010100&key=YOUR_KEY'

其中location可通过qwd/LocationList: 和风天气常用城市列表 (github.com)查询

key为你创建项目时自动创建的key image.png

编写代码

package com.example.telegram_bot;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import okhttp3.*;
import org.springframework.stereotype.Component;
import org.telegram.telegrambots.bots.DefaultBotOptions;
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
import org.telegram.telegrambots.meta.api.objects.CallbackQuery;
import org.telegram.telegrambots.meta.api.objects.Message;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;


/**
 * 长轮询方式部署机器人
 */
@Component
public class ExecBot extends TelegramLongPollingBot {

    // 填你自己的token和username
    private String botToken = "YOUR_TOKEN";
    private String botName = "YOUR_BOT_NAME";// newbot时你的第二个名字


    // 通义千问API Key
    private static final String QIANWEN_API_KEY = "YOUR_QIANWEN_KEY";
    // 请求地址
    private static final String QIANWEN_API_URL = "YOUR_QIANWEN_URL";

    private static final String WEATHER_API_URL = "YOUR_WEATHER_URL";
    private static final String API_KEY = "YOUR_WEATHER_KEY"; // 请替换为你的和风天气 API 密钥

    // 设置 OkHttpClient 的超时时间
    private static final OkHttpClient client = new OkHttpClient.Builder()
            .connectTimeout(60, TimeUnit.SECONDS) // 连接超时时间
            .readTimeout(60, TimeUnit.SECONDS)     // 读取超时时间
            .writeTimeout(60, TimeUnit.SECONDS)    // 写入超时时间
            .build();

    // 用于跟踪用户的会话状态
    private boolean isQwenChatMode = false;
    private boolean isWaitingForCity = false;

    public ExecBot() {
        this(new DefaultBotOptions());
    }

    // 通过 DefaultBotOptions 允许你指定代理服务器等配置。super(options) 将这个配置传给父类 TelegramLongPollingBot 进行处理
    public ExecBot(DefaultBotOptions options) {
        super(options);
    }

    @Override
    public String getBotToken() {
        return botToken;
    }

    @Override
    public String getBotUsername() {
        return botName;
    }

    
    @Override
    public void onUpdateReceived(Update update) {
        // 检查消息更新
        if (update.hasMessage()) {
            // 取出消息及聊天Id
            Message message = update.getMessage();
            Long chatId = message.getChatId();

            // 获取用户输入的消息文本
            String text = message.getText().trim();

            // 检查是否是 /search 命令
            if (text.equals("/search")) {
                sendSearchMenu(chatId);
            } else if (text.startsWith("/ask") && isQwenChatMode) {
                // 处理 /ask 命令,只有在与通义千问对话模式下才允许
                String question = text.substring(5).trim();
                String answer = getAnswerFromQianwen(question);
                sendMsg(answer, chatId);
            } else if (update.hasMessage() && isWaitingForCity) {
                // 用户输入了城市地点码
                message = update.getMessage();
                chatId = message.getChatId();
                String location = message.getText().trim();

                // 获取天气信息
                String weatherInfo = getWeatherNow(location);
                sendMsg(weatherInfo, chatId);

                // 重置等待状态
                isWaitingForCity = false;
            }else {
                // 其他消息处理
                if (isQwenChatMode) {
                    // 如果在对话模式下,直接将消息视为问题
                    String answer = getAnswerFromQianwen(text);
                    sendMsg(answer, chatId);
                } else {
                    sendMsg("请先使用 /search 命令调出菜单并选择与通义千问对话。", chatId);
                }
            }
        } else if (update.hasCallbackQuery()) {// 检查是否有回调查询
            // 处理回调查询
            CallbackQuery callbackQuery = update.getCallbackQuery();// 获取 CallbackQuery 对象
            String data = callbackQuery.getData();// 获取按钮的数据
            Long chatId = callbackQuery.getMessage().getChatId();// 获取消息的聊天 ID

            if ("qwen_chat".equals(data)) {
                // 用户点击了“与通义千问对话”按钮
                isQwenChatMode = true;
                sendMsg("你现在可以使用 /ask 问题 与通义千问对话。", chatId);
            } else if ("return_menu".equals(data)) {
                // 用户点击了“返回”按钮
                isQwenChatMode = false;
                sendSearchMenu(chatId);
            } else if ("weather_forecast".equals(data)) {
                // 用户点击了“天气预报”按钮
                sendMsg("请输入您想查询的城市地点码(例如:101280803):", chatId);
                // 设置等待用户输入城市地点码的状态
                isWaitingForCity = true;
            }else {
                // 处理其他按钮点击
                sendMsg("你点击了按钮: " + data, chatId);
            }
        }
    }

    // 调用通义千问API的方法
    private String getAnswerFromQianwen(String question) {
        // OkHttpClient 实例,用于发送 HTTP 请求
        OkHttpClient client = new OkHttpClient();
        /** 设置请求体
         * MediaType:指定请求的内容类型为 JSON
         * requestBody:创建一个 JsonObject 来存储请求体
         *              添加 "model" 属性,值为 "qwen-plus",表示调用的模型
         *              添加 "messages" 属性,值为通过 createMessages(question) 方法生成的消息数组
         */
        MediaType mediaType = MediaType.parse("application/json; charset=utf-8");
        JsonObject requestBody = new JsonObject();
        requestBody.addProperty("model", "qwen-plus");
        requestBody.add("messages", createMessages(question));

        /** 构建 HTTP 请求
         * 设置请求的 URL,指向通义千问 API 的 /chat/completions 端点
         * 使用 POST 方法发送请求,并设置请求体为 requestBody
         * 添加 Authorization 头,值为 Bearer <API Key>,用于身份验证
         * 构建最终的 Request 对象
         */
         Request request = new Request.Builder()
                .url(QIANWEN_API_URL + "/chat/completions")
                .post(RequestBody.create(mediaType, requestBody.toString()))
                .addHeader("Authorization", "Bearer " + QIANWEN_API_KEY)
                .build();

        int maxRetries = 3; // 最大重试次数
        for (int attempt = 0; attempt < maxRetries; attempt++) {
            // client.newCall(request).execute():执行 HTTP 请求并获取响应
            try (Response response = client.newCall(request).execute()) {
                // response.isSuccessful():检查响应是否成功(HTTP 状态码在 200-299 之间)
                if (response.isSuccessful() && response.body() != null) {
                    // response.body().string():读取响应体内容
                    String responseBody = response.body().string();
                    // JsonParser 和 Gson 解析 JSON 响应
                    JsonParser parser = new JsonParser();
                    JsonObject json = parser.parse(responseBody).getAsJsonObject();
                    // 提取 choices 数组中的第一个元素,并从中获取 message 的 content 字段,作为最终答案返回
                    JsonObject choice = json.getAsJsonArray("choices").get(0).getAsJsonObject();
                    return choice.getAsJsonObject("message").get("content").getAsString();
                } else {
                    return "抱歉,出现了一个错误。" + response.code();
                }
            } catch (IOException e) {
                if (attempt == maxRetries - 1) { // 如果是最后一次重试,抛出异常
                    e.printStackTrace();
                    return "抱歉,出现了一个错误。" + e.getMessage();
                }
                // 否则等待一段时间后重试
                try {
                    Thread.sleep(1000 * (attempt + 1)); // 等待时间逐渐增加
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException(ie);
                }
            }
        }
        return "抱歉,出现了一个错误。";
    }

    // 创建消息数组
    private JsonArray createMessages(String question) {
        Gson gson = new Gson();
        // systemMessage:创建一个 JsonObject 实例
        JsonObject systemMessage = new JsonObject();
        // 添加一个键为 "role"、值为 "system" 的属性,表示这是一个系统消息
        systemMessage.addProperty("role", "system");
        // 添加一个键为 "content"、值为 "You are a helpful assistant." 的属性,这是系统消息的内容
        systemMessage.addProperty("content", "You are a helpful assistant.");

        JsonObject userMessage = new JsonObject();
        // 添加一个键为 "role"、值为 "user" 的属性,表示这是一个用户消息
        userMessage.addProperty("role", "user");
        // 添加一个键为 "content"、值为 question 的属性,这是用户消息的内容(即用户提出的问题)
        userMessage.addProperty("content", question);

        JsonArray messages = new JsonArray();
        // 将系统消息对象、用户消息对象添加到 JSON 数组中
        messages.add(systemMessage);
        messages.add(userMessage);
        return messages;
    }

    // 回复消息
    public void sendMsg(String text, Long chatId) {
        // 使用 SendMessage 对象构建回复消息
        SendMessage response = new SendMessage();
        // 设置要发送消息的聊天对象、发送内容、消息内容支持 HTML 格式的解析
        response.setChatId(String.valueOf(chatId));
        response.setText(text);
        response.setParseMode("html");
        try {
            // 执行发送消息
            execute(response);
        } catch (TelegramApiException e) {
            e.getMessage();
        }
    }

    // 调出菜单
    private void sendSearchMenu(Long chatId) {
        InlineKeyboardMarkup markupInline = new InlineKeyboardMarkup();
        List<List<InlineKeyboardButton>> rowsInline = new ArrayList<>();

        // 第一行
        List<InlineKeyboardButton> row1 = new ArrayList<>();
        InlineKeyboardButton b1 = new InlineKeyboardButton();
        b1.setText("与通义千问对话");
        b1.setCallbackData("qwen_chat");
        InlineKeyboardButton b2 = new InlineKeyboardButton();
        b2.setText("选项2");
        b2.setCallbackData("option2");
        row1.add(b1);
        row1.add(b2);
        rowsInline.add(row1);

        // 第二行
        List<InlineKeyboardButton> row2 = new ArrayList<>();
        InlineKeyboardButton b3 = new InlineKeyboardButton();
        b3.setText("天气预报");
        b3.setCallbackData("weather_forecast");
        InlineKeyboardButton b4 = new InlineKeyboardButton();
        b4.setText("返回");
        b4.setCallbackData("return_menu");
        row1.add(b3);
        row1.add(b4);
        rowsInline.add(row2);

        markupInline.setKeyboard(rowsInline);

        SendMessage sendMessage = SendMessage.builder()
                .text("请选择一个选项!")
                .chatId(chatId)
                .replyMarkup(markupInline)
                .build();

        try {
            execute(sendMessage);
        } catch (TelegramApiException e) {
            e.printStackTrace();
        }
    }

    // 天气预报请求
    public static String getWeatherNow(String location) {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url(WEATHER_API_URL + "?location=" + location + "&key=" + API_KEY)
                .build();

        try (Response response = client.newCall(request).execute()) {
            if (response.isSuccessful() && response.body() != null) {
                String responseBody = response.body().string();
                JsonObject json = new Gson().fromJson(responseBody, JsonObject.class);
                JsonObject now = json.getAsJsonObject("now");
                return formatWeatherInfo(now);
            } else {
                return "抱歉,无法获取天气信息。" + response.code();
            }
        } catch (IOException e) {
            e.printStackTrace();
            return "抱歉,出现了一个错误。" + e.getMessage();
        }
    }

    // 天气预报返回结果JSON格式化
    private static String formatWeatherInfo(JsonObject now) {
        String text = now.get("text").getAsString(); // 天气状况
        double temp = now.get("temp").getAsDouble(); // 温度
        int humidity = now.get("humidity").getAsInt(); // 湿度
        double windSpeed = now.get("windSpeed").getAsDouble(); // 风速
        String windDir = now.get("windDir").getAsString(); // 风向

        return String.format("当前天气:\n温度: %.1f°C\n湿度: %d%%\n风速: %.1f km/h\n风向: %s\n描述: %s",
                temp, humidity, windSpeed, windDir, text);
    }
}

测试结果

image.png

总结

机器人的功能开发大体上都是使用apikey结合请求地址进行请求获取数据,相关的apikey与请求地址可通过官方开发文档获取,发挥你的想象,实现丰富的功能