让小智AI接入直播间作为语音助手
在Listening状态获取消息
检查当前会话是否已获取过
未获取则获取新消息并调用
WakeWordInvoke()标记已获取
处理完成判断
当设备回到Listening状态时,说明上一条消息已处理完成
立即通知服务器完成
重置标志,准备获取下一条
状态变化时重置
离开Listening状态时自动重置标志
⚡ 优势
不等待Idle:处理完立即回到Listening就能获取下一条
响应更快:消息处理周期大大缩短
逻辑简单:
Listening状态 → 获取消息
Speaking/Connecting → 处理中
回到Listening → 完成,获取下一条
这样消息队列处理会快很多!
#include "wifi_board.h"
#include "codecs/no_audio_codec.h"
#include "display/lcd_display.h"
#include "font_awesome_symbols.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "mcp_server.h"
#include "settings.h"
#include <wifi_station.h>
#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
#include <driver/i2c_master.h>
#include <driver/spi_common.h>
#include <driver/uart.h>
#include <cstring>
#include "emote_display.h"
#include "driver/temperature_sensor.h"
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <queue>
#include <string>
#include <unordered_map>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include <driver/spi_common.h>
#include <esp_lcd_ili9488.h>
#include <esp_http_client.h>
#include <esp_timer.h>
#define TAG "CompactWifiBoardLCD"
#define HTTP_NEXT_URL "http://192.168.1.90/YESHI/ai.php?action=next"
#define HTTP_COMPLETE_URL "http://192.168.1.90/YESHI/ai.php?action=complete&id=%d"
#define HTTP_GET_TIMEOUT_MS 5000
#define MESSAGE_FETCH_INTERVAL_MS 1000 // 定时器间隔:1秒
LV_FONT_DECLARE(font_puhui_20_4);
LV_FONT_DECLARE(font_awesome_20_4);
static const size_t LV_BUFFER_SIZE = 320 * 25;
#define USE_LVGL_DEFAULT 0
class CompactWifiBoardLCD : public WifiBoard {
private:
Button boot_button_;
i2c_master_bus_handle_t i2c_bus_;
Display* display_;
esp_timer_handle_t message_timer_ = nullptr;
bool is_fetching_messages_ = false;
int current_message_id_ = 0;
bool is_processing_message_ = false;
bool has_fetched_in_current_idle_ = false; // 当前空闲会话是否已获取消息
// HTTP响应数据结构
struct HttpResponseData {
char* buffer;
size_t size;
size_t capacity;
};
// HTTP事件处理回调
static esp_err_t HttpEventHandler(esp_http_client_event_t *evt) {
HttpResponseData *data = (HttpResponseData *)evt->user_data;
switch(evt->event_id) {
case HTTP_EVENT_ON_DATA:
if (data->buffer == nullptr) {
data->capacity = 1024;
data->buffer = (char*)malloc(data->capacity);
data->size = 0;
}
if (data->size + evt->data_len >= data->capacity) {
data->capacity = data->size + evt->data_len + 1024;
data->buffer = (char*)realloc(data->buffer, data->capacity);
}
if (data->buffer) {
memcpy(data->buffer + data->size, evt->data, evt->data_len);
data->size += evt->data_len;
data->buffer[data->size] = '\0';
}
break;
default:
break;
}
return ESP_OK;
}
// 从服务器获取下一条消息
bool FetchNextMessage(char* out_message, size_t max_len, int* out_message_id) {
if (!out_message || max_len == 0 || !out_message_id) {
return false;
}
HttpResponseData response_data = {0};
bool success = false;
esp_http_client_config_t config = {};
config.url = HTTP_NEXT_URL;
config.event_handler = HttpEventHandler;
config.user_data = &response_data;
config.timeout_ms = HTTP_GET_TIMEOUT_MS;
esp_http_client_handle_t client = esp_http_client_init(&config);
if (!client) {
ESP_LOGE(TAG, "HTTP客户端初始化失败");
return false;
}
esp_err_t err = esp_http_client_perform(client);
if (err == ESP_OK) {
int status_code = esp_http_client_get_status_code(client);
if (status_code == 200 && response_data.buffer && response_data.size > 0) {
// 解析格式: "消息ID|消息内容"
char* separator = strchr(response_data.buffer, '|');
if (separator) {
*separator = '\0';
*out_message_id = atoi(response_data.buffer);
char* message_content = separator + 1;
size_t content_len = strlen(message_content);
size_t copy_len = (content_len < max_len - 1) ? content_len : max_len - 1;
memcpy(out_message, message_content, copy_len);
out_message[copy_len] = '\0';
// 去除首尾空白字符
char* start = out_message;
while (*start && (*start == ' ' || *start == '\n' || *start == '\r' || *start == '\t')) {
start++;
}
if (start != out_message) {
memmove(out_message, start, strlen(start) + 1);
}
size_t len = strlen(out_message);
while (len > 0 && (out_message[len-1] == ' ' || out_message[len-1] == '\n' ||
out_message[len-1] == '\r' || out_message[len-1] == '\t')) {
out_message[--len] = '\0';
}
if (strlen(out_message) > 0) {
// ID可以是0(随机话题)或大于0(真实消息)
ESP_LOGI(TAG, "收到消息 [ID=%d]: %s", *out_message_id, out_message);
success = true;
} else {
ESP_LOGW(TAG, "消息内容为空");
}
} else {
ESP_LOGW(TAG, "消息格式错误,缺少分隔符");
}
} else if (status_code == 200) {
ESP_LOGD(TAG, "服务器返回空响应");
} else {
ESP_LOGW(TAG, "HTTP状态码异常: %d", status_code);
}
} else {
ESP_LOGE(TAG, "HTTP GET请求失败: %s", esp_err_to_name(err));
}
if (response_data.buffer) {
free(response_data.buffer);
}
esp_http_client_cleanup(client);
return success;
}
// 通知服务器消息处理完成
bool NotifyMessageComplete(int message_id) {
char complete_url[256];
snprintf(complete_url, sizeof(complete_url), HTTP_COMPLETE_URL, message_id);
esp_http_client_config_t config = {};
config.url = complete_url;
config.method = HTTP_METHOD_GET;
config.timeout_ms = 3000;
esp_http_client_handle_t client = esp_http_client_init(&config);
if (!client) {
ESP_LOGE(TAG, "HTTP客户端初始化失败");
return false;
}
esp_err_t err = esp_http_client_perform(client);
bool success = false;
if (err == ESP_OK) {
int status_code = esp_http_client_get_status_code(client);
if (status_code == 200) {
ESP_LOGI(TAG, "消息完成通知成功: ID=%d", message_id);
success = true;
} else {
ESP_LOGW(TAG, "消息完成通知失败: 状态码=%d", status_code);
}
} else {
ESP_LOGE(TAG, "消息完成通知请求失败: %s", esp_err_to_name(err));
}
esp_http_client_cleanup(client);
return success;
}
// 定时器回调函数(借鉴Esp32LiveStream的设计)
static void MessageTimerCallback(void* arg) {
CompactWifiBoardLCD* self = static_cast<CompactWifiBoardLCD*>(arg);
// 检查是否停止获取
if (!self->is_fetching_messages_) {
ESP_LOGD(TAG, "消息获取已停止,忽略定时器回调");
return;
}
// 获取当前设备状态
auto& app = Application::GetInstance();
DeviceState device_state = app.GetDeviceState();
ESP_LOGD(TAG, "定时器触发 - 状态: %d, 正在处理: %d, 已获取: %d",
device_state, self->is_processing_message_, self->has_fetched_in_current_idle_);
// 情况1:正在处理消息,检查是否回到Listening状态(说明处理完成)
if (self->is_processing_message_) {
// 检查是否回到Listening状态,说明上一条消息处理完成
if (device_state == kDeviceStateListening) {
ESP_LOGI(TAG, "回到Listening状态,消息处理完成");
// 只有ID>0的消息才需要通知服务器(ID=0是随机话题)
if (self->current_message_id_ > 0) {
ESP_LOGI(TAG, "通知服务器消息完成: ID=%d", self->current_message_id_);
if (self->NotifyMessageComplete(self->current_message_id_)) {
self->is_processing_message_ = false;
self->current_message_id_ = 0;
self->has_fetched_in_current_idle_ = false;
ESP_LOGI(TAG, "准备获取下一条消息");
} else {
ESP_LOGW(TAG, "完成通知失败,下次定时器重试");
}
} else {
// 随机话题处理完成,无需通知服务器
ESP_LOGI(TAG, "随机话题处理完成,无需通知服务器");
self->is_processing_message_ = false;
self->current_message_id_ = 0;
self->has_fetched_in_current_idle_ = false;
}
}
return; // 正在处理中,不获取新消息
}
// 情况2:没有正在处理的消息,检查是否可以获取新消息
// 在Listening状态下获取
if (device_state == kDeviceStateListening) {
// 检查当前Listening会话是否已获取过消息
if (!self->has_fetched_in_current_idle_) {
ESP_LOGI(TAG, "设备处于Listening状态且未获取过消息,尝试获取新消息");
char message_buffer[512];
int fetched_message_id = 0;
if (self->FetchNextMessage(message_buffer, sizeof(message_buffer), &fetched_message_id)) {
if (fetched_message_id == 0) {
ESP_LOGI(TAG, "获取到随机话题: %s", message_buffer);
} else {
ESP_LOGI(TAG, "获取到消息 [ID=%d]: %s", fetched_message_id, message_buffer);
}
// 标记正在处理
self->is_processing_message_ = true;
self->current_message_id_ = fetched_message_id;
self->has_fetched_in_current_idle_ = true; // 标记当前会话已获取
// 根据设备状态选择处理方式
if (device_state == kDeviceStateListening) {
ESP_LOGI(TAG, "设备处于Listening状态,使用 SendWakeWordDetected 处理");
Protocol* protocol = &Application::GetInstance().GetProtocol();
protocol->SendWakeWordDetected(message_buffer);
} else {
ESP_LOGI(TAG, "设备不在Listening状态,使用 WakeWordInvoke 处理");
app.WakeWordInvoke(message_buffer);
}
} else {
ESP_LOGD(TAG, "获取消息失败,等待下次定时器");
}
} else {
ESP_LOGD(TAG, "当前Listening会话已获取过消息,等待状态变化");
}
} else {
// 不在Listening状态,重置获取标志
if (self->has_fetched_in_current_idle_ && device_state != kDeviceStateListening) {
ESP_LOGD(TAG, "设备离开Listening状态,重置获取标志");
self->has_fetched_in_current_idle_ = false;
}
}
}
// 启动消息获取定时器
bool StartMessageFetching() {
if (is_fetching_messages_) {
ESP_LOGW(TAG, "消息获取已在运行");
return true;
}
ESP_LOGI(TAG, "启动消息获取定时器...");
// 创建定时器配置
esp_timer_create_args_t timer_args = {
.callback = MessageTimerCallback,
.arg = this,
.dispatch_method = ESP_TIMER_TASK,
.name = "message_fetch_timer"
};
// 创建定时器
esp_err_t ret = esp_timer_create(&timer_args, &message_timer_);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "创建定时器失败: %s", esp_err_to_name(ret));
return false;
}
// 启动定时器(每秒触发一次)
ret = esp_timer_start_periodic(message_timer_, MESSAGE_FETCH_INTERVAL_MS * 1000);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "启动定时器失败: %s", esp_err_to_name(ret));
esp_timer_delete(message_timer_);
message_timer_ = nullptr;
return false;
}
is_fetching_messages_ = true;
has_fetched_in_current_idle_ = false;
ESP_LOGI(TAG, "消息获取定时器启动成功,间隔: %d ms", MESSAGE_FETCH_INTERVAL_MS);
return true;
}
// 停止消息获取
void StopMessageFetching() {
if (!is_fetching_messages_) {
return;
}
ESP_LOGI(TAG, "停止消息获取定时器");
is_fetching_messages_ = false;
if (message_timer_) {
esp_timer_stop(message_timer_);
esp_timer_delete(message_timer_);
message_timer_ = nullptr;
}
has_fetched_in_current_idle_ = false;
ESP_LOGI(TAG, "消息获取定时器已停止");
}
void InitializeSpi() {
spi_bus_config_t buscfg = {};
buscfg.mosi_io_num = DISPLAY_MOSI_PIN;
buscfg.miso_io_num = GPIO_NUM_NC;
buscfg.sclk_io_num = DISPLAY_CLK_PIN;
buscfg.quadwp_io_num = GPIO_NUM_NC;
buscfg.quadhd_io_num = GPIO_NUM_NC;
buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO));
}
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
}
app.ToggleChatState();
});
}
void InitializeDisplay() {
esp_lcd_panel_io_handle_t panel_io = nullptr;
esp_lcd_panel_handle_t panel = nullptr;
ESP_LOGD(TAG, "Install panel IO");
esp_lcd_panel_io_spi_config_t io_config = {};
io_config.cs_gpio_num = DISPLAY_CS_PIN;
io_config.dc_gpio_num = DISPLAY_DC_PIN;
io_config.spi_mode = DISPLAY_SPI_MODE;
io_config.pclk_hz = 40 * 1000 * 1000;
io_config.trans_queue_depth = 10;
io_config.lcd_cmd_bits = 8;
io_config.lcd_param_bits = 8;
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io));
ESP_LOGD(TAG, "Install LCD driver");
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = DISPLAY_RST_PIN;
panel_config.rgb_ele_order = DISPLAY_RGB_ORDER;
panel_config.bits_per_pixel = 18;
ESP_ERROR_CHECK(esp_lcd_new_panel_ili9488(panel_io, &panel_config, LV_BUFFER_SIZE, &panel));
esp_lcd_panel_reset(panel);
esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, false);
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
esp_lcd_panel_disp_on_off(panel, true);
#if USE_LVGL_DEFAULT
display_ = new SpiLcdDisplay(panel_io, panel,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y,
DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY,
{
.text_font = &font_puhui_20_4,
.icon_font = &font_awesome_20_4,
.emoji_font = font_emoji_64_init(),
});
#else
display_ = new anim::EmoteDisplay(panel, panel_io);
#endif
}
public:
CompactWifiBoardLCD() : boot_button_(BOOT_BUTTON_GPIO) {
InitializeSpi();
InitializeDisplay();
InitializeButtons();
if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) {
GetBacklight()->RestoreBrightness();
}
// 延迟启动消息获取定时器
xTaskCreate([](void* param) {
CompactWifiBoardLCD* self = (CompactWifiBoardLCD*)param;
// 等待WiFi稳定连接(最多30秒)
int wifi_wait_count = 0;
while (!WifiStation::GetInstance().IsConnected() && wifi_wait_count < 30) {
vTaskDelay(pdMS_TO_TICKS(1000));
wifi_wait_count++;
}
if (WifiStation::GetInstance().IsConnected()) {
// 再等待3秒确保系统稳定
vTaskDelay(pdMS_TO_TICKS(3000));
self->StartMessageFetching();
} else {
ESP_LOGW(TAG, "WiFi未连接,跳过消息获取启动");
}
vTaskDelete(NULL);
}, "msg_init", 4096, this, 1, NULL);
}
virtual ~CompactWifiBoardLCD() {
StopMessageFetching();
}
virtual AudioCodec* GetAudioCodec() override {
static NoAudioCodecSimplex audio_codec(
AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, I2S_STD_SLOT_RIGHT,
AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN, I2S_STD_SLOT_LEFT);
return &audio_codec;
}
virtual Display* GetDisplay() override {
return display_;
}
virtual Backlight* GetBacklight() override {
if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) {
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
return &backlight;
}
return nullptr;
}
};
DECLARE_BOARD(CompactWifiBoardLCD);<?php
// 基础配置
header('Content-Type: application/json; charset=utf-8');
header('X-XSS-Protection: 1; mode=block');
// 数据库配置
define('DB_FILE', __DIR__ . '/ai_messages.db');
// 工具函数:获取客户端IP
function getClientIP() {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '未知IP';
return trim(explode(',', $ip)[0]);
}
// 工具函数:输入清洗
function cleanStr($str) {
if (!is_string($str)) $str = (string)$str;
$str = trim($str);
$str = htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
return $str;
}
// 工具函数:格式化消息为嵌入式支持的字符串
function formatToDeviceStr($messageData) {
$username = cleanStr($messageData['username'] ?? '未知用户');
$content = cleanStr($messageData['content'] ?? '');
$msgType = cleanStr($messageData['type'] ?? 'comment');
switch ($msgType) {
case 'gift':
$giftName = cleanStr($messageData['giftName'] ?? '礼物');
$giftCount = is_numeric($messageData['giftCount'] ?? 1) ? (int)$messageData['giftCount'] : 1;
$countStr = $giftCount > 1 ? "{$giftCount}个" : "1个";
return "{$username}送给主播{$countStr}{$giftName}";
case 'enter':
return "{$username}来了";
case 'like':
return "{$username}给主播点赞";
case 'comment':
default:
return "{$username}说:{$content}";
}
}
// 初始化数据库
function initDatabase() {
$db = new SQLite3(DB_FILE);
// 创建消息队列表
$db->exec("
CREATE TABLE IF NOT EXISTS message_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message_text TEXT NOT NULL,
message_type VARCHAR(20) DEFAULT 'comment',
username VARCHAR(100),
raw_data TEXT,
client_ip VARCHAR(50),
status VARCHAR(20) DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
processing_at DATETIME,
processed_at DATETIME
)
");
// 创建索引提升查询性能
$db->exec("CREATE INDEX IF NOT EXISTS idx_status ON message_queue(status)");
$db->exec("CREATE INDEX IF NOT EXISTS idx_created_at ON message_queue(created_at)");
$db->close();
}
// 添加消息到队列
function addMessageToQueue($messageData, $deviceStr, $clientIP) {
$db = new SQLite3(DB_FILE);
$stmt = $db->prepare("
INSERT INTO message_queue (message_text, message_type, username, raw_data, client_ip, status)
VALUES (:message_text, :message_type, :username, :raw_data, :client_ip, 'pending')
");
$stmt->bindValue(':message_text', $deviceStr, SQLITE3_TEXT);
$stmt->bindValue(':message_type', $messageData['type'] ?? 'comment', SQLITE3_TEXT);
$stmt->bindValue(':username', $messageData['username'] ?? '未知用户', SQLITE3_TEXT);
$stmt->bindValue(':raw_data', json_encode($messageData, JSON_UNESCAPED_UNICODE), SQLITE3_TEXT);
$stmt->bindValue(':client_ip', $clientIP, SQLITE3_TEXT);
$result = $stmt->execute();
$insertId = $db->lastInsertRowID();
$db->close();
return $insertId;
}
// 获取下一条待处理消息
function getNextMessage() {
$db = new SQLite3(DB_FILE);
// 查询最早的待处理消息
$stmt = $db->prepare("
SELECT id, message_text, message_type, username, created_at
FROM message_queue
WHERE status = 'pending'
ORDER BY created_at ASC
LIMIT 1
");
$result = $stmt->execute();
$message = $result->fetchArray(SQLITE3_ASSOC);
// 如果找到消息,标记为处理中
if ($message) {
$updateStmt = $db->prepare("
UPDATE message_queue
SET status = 'processing', processing_at = CURRENT_TIMESTAMP
WHERE id = :id
");
$updateStmt->bindValue(':id', $message['id'], SQLITE3_INTEGER);
$updateStmt->execute();
}
$db->close();
return $message;
}
// 获取随机话题
function getRandomTopic() {
$topics = [
"今天天气真不错呀",
"大家有什么想聊的吗",
"有没有人在看直播呢",
"来个朋友陪我聊聊天吧",
"好无聊啊,有人吗",
"今天直播间好安静呀",
"大家都在忙什么呢",
"有没有新朋友来玩呀",
"老朋友们在哪里",
"来点互动好不好",
"发个666让我看看你们",
"有人想听我唱歌吗",
"今天想和大家分享点什么",
"直播间的氛围好像有点冷清",
"来来来,活跃一下气氛",
"有什么想问我的吗",
"今天过得怎么样呀",
"周末有什么计划吗",
"最近有什么好玩的事情",
"给大家讲个笑话好不好"
];
$randomIndex = array_rand($topics);
return $topics[$randomIndex];
}
// 标记消息为已完成并删除
function markMessageComplete($messageId) {
$db = new SQLite3(DB_FILE);
// 先标记为已完成(可选,用于统计)
$stmt = $db->prepare("
UPDATE message_queue
SET status = 'completed', processed_at = CURRENT_TIMESTAMP
WHERE id = :id
");
$stmt->bindValue(':id', $messageId, SQLITE3_INTEGER);
$stmt->execute();
// 删除已完成的消息
$deleteStmt = $db->prepare("DELETE FROM message_queue WHERE id = :id");
$deleteStmt->bindValue(':id', $messageId, SQLITE3_INTEGER);
$deleteStmt->execute();
$db->close();
return true;
}
// 获取队列统计信息
function getQueueStats() {
$db = new SQLite3(DB_FILE);
$result = $db->query("
SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
SUM(CASE WHEN status = 'processing' THEN 1 ELSE 0 END) as processing
FROM message_queue
");
$stats = $result->fetchArray(SQLITE3_ASSOC);
$db->close();
return $stats;
}
// 清理长时间卡住的处理中消息(防止死锁)
function cleanupStuckMessages($timeoutMinutes = 5) {
$db = new SQLite3(DB_FILE);
$stmt = $db->prepare("
UPDATE message_queue
SET status = 'pending', processing_at = NULL
WHERE status = 'processing'
AND datetime(processing_at, '+' || :timeout || ' minutes') < datetime('now')
");
$stmt->bindValue(':timeout', $timeoutMinutes, SQLITE3_INTEGER);
$stmt->execute();
$affected = $db->changes();
$db->close();
return $affected;
}
// ---------------------- 主逻辑 ----------------------
// 初始化数据库
initDatabase();
$clientIP = getClientIP();
$action = $_GET['action'] ?? '';
// 处理不同的action
switch ($action) {
case 'add':
// 添加新消息到队列(POST请求)
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => '仅支持POST请求']);
exit;
}
$rawPost = file_get_contents('php://input');
if (empty($rawPost)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'POST数据为空']);
exit;
}
$postData = json_decode($rawPost, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$postData = ['type' => 'comment', 'content' => $rawPost, 'username' => '未知用户'];
}
$deviceStr = formatToDeviceStr($postData);
$insertId = addMessageToQueue($postData, $deviceStr, $clientIP);
$stats = getQueueStats();
echo json_encode([
'status' => 'success',
'message' => '消息已加入队列',
'data' => [
'queue_id' => $insertId,
'message' => $deviceStr,
'queue_stats' => $stats
]
]);
break;
case 'next':
// AI设备请求下一条消息(GET请求)
cleanupStuckMessages(5); // 清理超过5分钟未完成的消息
$message = getNextMessage();
if ($message) {
// 返回格式:消息ID|消息内容
// 这样ESP32可以解析出ID用于后续完成通知
header('Content-Type: text/plain; charset=utf-8');
echo $message['id'] . '|' . $message['message_text'];
} else {
// 队列为空,返回随机话题
$randomTopic = getRandomTopic();
// 使用特殊ID=0表示这是随机话题,不需要完成通知
header('Content-Type: text/plain; charset=utf-8');
echo '0|' . $randomTopic;
// 记录日志(可选)
error_log("[AI Queue] 队列为空,返回随机话题: " . $randomTopic);
}
break;
case 'complete':
// AI设备完成处理,删除消息(POST或GET请求)
$messageId = $_GET['id'] ?? $_POST['id'] ?? null;
if (!$messageId || !is_numeric($messageId)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => '缺少消息ID']);
exit;
}
markMessageComplete((int)$messageId);
$stats = getQueueStats();
echo json_encode([
'status' => 'success',
'message' => '消息已标记完成并删除',
'data' => [
'completed_id' => (int)$messageId,
'queue_stats' => $stats
]
]);
break;
case 'stats':
// 获取队列统计信息
$stats = getQueueStats();
echo json_encode([
'status' => 'success',
'data' => $stats
]);
break;
case 'clear':
// 清空队列(危险操作,可选)
$db = new SQLite3(DB_FILE);
$db->exec("DELETE FROM message_queue");
$affected = $db->changes();
$db->close();
echo json_encode([
'status' => 'success',
'message' => "已清空队列,删除 {$affected} 条消息"
]);
break;
default:
http_response_code(400);
echo json_encode([
'status' => 'error',
'message' => '无效的action参数',
'available_actions' => [
'add' => '添加消息到队列(POST)',
'next' => 'AI获取下一条消息(GET)',
'complete' => '标记消息完成(GET/POST,需要id参数)',
'stats' => '获取队列统计(GET)',
'clear' => '清空队列(GET)'
]
]);
}
?>扫描二维码推送至手机访问。
本サイト上に掲載の文章、画像、写真などを無断で複製することは法律で禁じられています。全ての著作権はGAMESHに帰属します。


