实现每机器人独立 LLM 消息队列
主要变更: - 后端:为 botNodeRecord 添加 llm_queue_enabled 和 llm_include_channel_messages 字段 - 后端:移除全局 LLM 队列开关,完全基于每个机器人的设置 - 后端:重写 enqueueChannelMessageToLLM,为每个启用了「包含频道消息」的机器人创建独立队列记录 - 后端:频道消息不再使用 bot_id=0,每条消息都有明确的机器人归属 - 前端:AdminLLM.vue 完全重写,添加机器人 LLM 设置面板 - 前端:消息列表表格新增「机器人」列,显示机器人名称和节点 ID - 前端:消息类型判断逻辑改为使用 channel_id 区分频道/私聊 - 前后端类型同步更新
This commit is contained in:
+4
-2
@@ -19,6 +19,8 @@ type botNodeRequest struct {
|
||||
PSK string `json:"psk"`
|
||||
NodeInfoBroadcastEnabled bool `json:"nodeinfo_broadcast_enabled"`
|
||||
NodeInfoBroadcastIntervalSeconds int64 `json:"nodeinfo_broadcast_interval_seconds"`
|
||||
LLMQueueEnabled bool `json:"llm_queue_enabled"`
|
||||
LLMIncludeChannelMessages bool `json:"llm_include_channel_messages"`
|
||||
}
|
||||
|
||||
type botSendMessageRequest struct {
|
||||
@@ -253,7 +255,7 @@ func registerAdminBotRoutes(r gin.IRouter, store *store, sender botTextSender) {
|
||||
}
|
||||
|
||||
func botNodeInputFromRequest(req botNodeRequest) botNodeInput {
|
||||
return botNodeInput{NodeNum: req.NodeNum, LongName: req.LongName, ShortName: req.ShortName, Enabled: req.Enabled, DefaultChannelID: req.DefaultChannelID, TopicPrefix: req.TopicPrefix, PSK: req.PSK, NodeInfoBroadcastEnabled: req.NodeInfoBroadcastEnabled, NodeInfoBroadcastIntervalSeconds: req.NodeInfoBroadcastIntervalSeconds}
|
||||
return botNodeInput{NodeNum: req.NodeNum, LongName: req.LongName, ShortName: req.ShortName, Enabled: req.Enabled, DefaultChannelID: req.DefaultChannelID, TopicPrefix: req.TopicPrefix, PSK: req.PSK, NodeInfoBroadcastEnabled: req.NodeInfoBroadcastEnabled, NodeInfoBroadcastIntervalSeconds: req.NodeInfoBroadcastIntervalSeconds, LLMQueueEnabled: req.LLMQueueEnabled, LLMIncludeChannelMessages: req.LLMIncludeChannelMessages}
|
||||
}
|
||||
|
||||
func parseBotID(c *gin.Context, message string) (uint64, bool) {
|
||||
@@ -299,7 +301,7 @@ func writeBotNodeMutationResponse(c *gin.Context, status int, row *botNodeRecord
|
||||
}
|
||||
|
||||
func botNodeDTO(row botNodeRecord) gin.H {
|
||||
return gin.H{"id": row.ID, "node_id": row.NodeID, "node_num": row.NodeNum, "long_name": row.LongName, "short_name": row.ShortName, "enabled": row.Enabled, "default_channel_id": row.DefaultChannelID, "topic_prefix": row.TopicPrefix, "psk": row.PSK, "public_key": row.PublicKey, "private_key_set": row.PrivateKey != "", "nodeinfo_broadcast_enabled": row.NodeInfoBroadcastEnabled, "nodeinfo_broadcast_interval_seconds": row.NodeInfoBroadcastIntervalSeconds, "last_nodeinfo_broadcast_at": row.LastNodeInfoBroadcastAt, "created_at": row.CreatedAt, "updated_at": row.UpdatedAt}
|
||||
return gin.H{"id": row.ID, "node_id": row.NodeID, "node_num": row.NodeNum, "long_name": row.LongName, "short_name": row.ShortName, "enabled": row.Enabled, "default_channel_id": row.DefaultChannelID, "topic_prefix": row.TopicPrefix, "psk": row.PSK, "public_key": row.PublicKey, "private_key_set": row.PrivateKey != "", "nodeinfo_broadcast_enabled": row.NodeInfoBroadcastEnabled, "nodeinfo_broadcast_interval_seconds": row.NodeInfoBroadcastIntervalSeconds, "last_nodeinfo_broadcast_at": row.LastNodeInfoBroadcastAt, "llm_queue_enabled": row.LLMQueueEnabled, "llm_include_channel_messages": row.LLMIncludeChannelMessages, "created_at": row.CreatedAt, "updated_at": row.UpdatedAt}
|
||||
}
|
||||
|
||||
func botMessageDTO(row botMessageRecord) gin.H {
|
||||
|
||||
+5
-1
@@ -39,6 +39,8 @@ type botNodeInput struct {
|
||||
PSK string
|
||||
NodeInfoBroadcastEnabled bool
|
||||
NodeInfoBroadcastIntervalSeconds int64
|
||||
LLMQueueEnabled bool
|
||||
LLMIncludeChannelMessages bool
|
||||
}
|
||||
|
||||
type botMessageListOptions struct {
|
||||
@@ -126,6 +128,8 @@ func (s *store) UpdateBotNode(id uint64, input botNodeInput) (*botNodeRecord, er
|
||||
"psk": row.PSK,
|
||||
"nodeinfo_broadcast_enabled": row.NodeInfoBroadcastEnabled,
|
||||
"nodeinfo_broadcast_interval_seconds": row.NodeInfoBroadcastIntervalSeconds,
|
||||
"llm_queue_enabled": row.LLMQueueEnabled,
|
||||
"llm_include_channel_messages": row.LLMIncludeChannelMessages,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
if err := s.db.Model(&botNodeRecord{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||||
@@ -276,7 +280,7 @@ func (s *store) normalizedBotNodeRecord(input botNodeInput) (*botNodeRecord, err
|
||||
if err := validateBotNodeNum(nodeNum); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &botNodeRecord{NodeID: mqtpp.NodeNumToID(uint32(nodeNum)), NodeNum: nodeNum, LongName: longName, ShortName: shortName, Enabled: input.Enabled, DefaultChannelID: channelID, TopicPrefix: topicPrefix, PSK: psk, NodeInfoBroadcastEnabled: input.NodeInfoBroadcastEnabled, NodeInfoBroadcastIntervalSeconds: interval}, nil
|
||||
return &botNodeRecord{NodeID: mqtpp.NodeNumToID(uint32(nodeNum)), NodeNum: nodeNum, LongName: longName, ShortName: shortName, Enabled: input.Enabled, DefaultChannelID: channelID, TopicPrefix: topicPrefix, PSK: psk, NodeInfoBroadcastEnabled: input.NodeInfoBroadcastEnabled, NodeInfoBroadcastIntervalSeconds: interval, LLMQueueEnabled: input.LLMQueueEnabled, LLMIncludeChannelMessages: input.LLMIncludeChannelMessages}, nil
|
||||
}
|
||||
|
||||
func populateBotNodeKeys(row *botNodeRecord) error {
|
||||
|
||||
@@ -266,6 +266,8 @@ type botNodeRecord struct {
|
||||
NodeInfoBroadcastIntervalSeconds int64 `gorm:"column:nodeinfo_broadcast_interval_seconds;not null"`
|
||||
LastNodeInfoBroadcastAt *time.Time `gorm:"column:last_nodeinfo_broadcast_at;index"`
|
||||
LastPacketID int64 `gorm:"column:last_packet_id;not null"`
|
||||
LLMQueueEnabled bool `gorm:"column:llm_queue_enabled;not null;default:1;index"`
|
||||
LLMIncludeChannelMessages bool `gorm:"column:llm_include_channel_messages;not null;default:0;index"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;index"`
|
||||
}
|
||||
@@ -667,6 +669,16 @@ func migrateBotNodePSK(tx *gorm.DB, migrator gorm.Migrator, driver string) error
|
||||
return fmt.Errorf("migrate bot_nodes last_nodeinfo_broadcast_at column: %w", err)
|
||||
}
|
||||
}
|
||||
if !migrator.HasColumn(&botNodeRecord{}, "LLMQueueEnabled") {
|
||||
if err := tx.Exec("ALTER TABLE bot_nodes ADD COLUMN llm_queue_enabled numeric NOT NULL DEFAULT 1").Error; err != nil {
|
||||
return fmt.Errorf("migrate bot_nodes llm_queue_enabled column: %w", err)
|
||||
}
|
||||
}
|
||||
if !migrator.HasColumn(&botNodeRecord{}, "LLMIncludeChannelMessages") {
|
||||
if err := tx.Exec("ALTER TABLE bot_nodes ADD COLUMN llm_include_channel_messages numeric NOT NULL DEFAULT 0").Error; err != nil {
|
||||
return fmt.Errorf("migrate bot_nodes llm_include_channel_messages column: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+24
-29
@@ -27,24 +27,19 @@ type LLMMessageQueueInput struct {
|
||||
|
||||
// EnqueueLLMMessage 将消息添加到 LLM 队列
|
||||
func (s *store) EnqueueLLMMessage(input LLMMessageQueueInput) (*llmMessageQueueRecord, error) {
|
||||
// 检查 LLM 队列是否启用
|
||||
enabled, err := s.GetBoolRuntimeSetting(runtimeSettingLLMQueueEnabled, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check llm queue enabled: %w", err)
|
||||
}
|
||||
if !enabled {
|
||||
return nil, nil // 静默返回,不报错
|
||||
var err error
|
||||
|
||||
if input.BotID == 0 {
|
||||
return nil, nil // bot_id 为 0 的消息不再入队
|
||||
}
|
||||
|
||||
// 如果是频道消息,检查是否启用了频道消息入队
|
||||
if input.BotID == 0 {
|
||||
includeChannel, err := s.GetBoolRuntimeSetting(runtimeSettingLLMQueueIncludeChannel, false)
|
||||
// 检查机器人级别的 LLM 队列设置
|
||||
bot, err := s.GetBotNode(input.BotID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check llm include channel: %w", err)
|
||||
}
|
||||
if !includeChannel {
|
||||
return nil, nil // 频道消息入队未启用,静默返回
|
||||
return nil, nil // 机器人不存在,静默返回
|
||||
}
|
||||
if !bot.LLMQueueEnabled {
|
||||
return nil, nil // 机器人的 LLM 队列未启用,静默返回
|
||||
}
|
||||
|
||||
if input.FromNodeID == "" {
|
||||
@@ -54,21 +49,10 @@ func (s *store) EnqueueLLMMessage(input LLMMessageQueueInput) (*llmMessageQueueR
|
||||
return nil, fmt.Errorf("text is required")
|
||||
}
|
||||
|
||||
// 检查是否存在重复消息
|
||||
// 检查是否存在重复消息:相同 bot_id + packet_id 且未删除
|
||||
var existing llmMessageQueueRecord
|
||||
if input.BotID > 0 {
|
||||
// 机器人私聊:相同 bot_id + packet_id 且未删除
|
||||
err = s.db.Where("bot_id = ? AND packet_id = ? AND deleted_at IS NULL", input.BotID, input.PacketID).
|
||||
Take(&existing).Error
|
||||
} else {
|
||||
// 频道消息:相同 from_node_id + channel_id + packet_id 且未删除
|
||||
channelID := ""
|
||||
if input.ChannelID != nil {
|
||||
channelID = *input.ChannelID
|
||||
}
|
||||
err = s.db.Where("bot_id = 0 AND from_node_id = ? AND channel_id = ? AND packet_id = ? AND deleted_at IS NULL", input.FromNodeID, channelID, input.PacketID).
|
||||
Take(&existing).Error
|
||||
}
|
||||
if err == nil {
|
||||
// 重复消息,直接返回已存在的
|
||||
return &existing, nil
|
||||
@@ -214,6 +198,7 @@ func llmMessageDTO(row llmMessageQueueRecord) map[string]any {
|
||||
}
|
||||
|
||||
// enqueueChannelMessageToLLM 将频道消息添加到 LLM 队列
|
||||
// 为每个启用了「包含频道消息」的机器人都创建一条独立的队列记录
|
||||
func enqueueChannelMessageToLLM(s *store, record map[string]any) error {
|
||||
if s == nil {
|
||||
return nil
|
||||
@@ -261,10 +246,19 @@ func enqueueChannelMessageToLLM(s *store, record map[string]any) error {
|
||||
contentPtr = &s
|
||||
}
|
||||
|
||||
// 查询所有启用了 LLM 队列且包含频道消息的机器人
|
||||
var bots []botNodeRecord
|
||||
err = s.db.Where("llm_queue_enabled = ? AND llm_include_channel_messages = ?", true, true).Find(&bots).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("query bots for channel message enqueue: %w", err)
|
||||
}
|
||||
|
||||
// 为每个符合条件的机器人创建一条队列记录
|
||||
for _, bot := range bots {
|
||||
_, _ = s.EnqueueLLMMessage(LLMMessageQueueInput{
|
||||
BotID: 0, // 0 表示频道消息
|
||||
BotNodeID: "",
|
||||
BotNodeNum: 0,
|
||||
BotID: bot.ID,
|
||||
BotNodeID: bot.NodeID,
|
||||
BotNodeNum: bot.NodeNum,
|
||||
FromNodeID: fromNodeID,
|
||||
FromNodeNum: fromNodeNum,
|
||||
LongName: longName,
|
||||
@@ -275,6 +269,7 @@ func enqueueChannelMessageToLLM(s *store, record map[string]any) error {
|
||||
Topic: topic,
|
||||
ContentJSON: contentPtr,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { deleteLLMMessage, deleteLLMMessagesByBot, getAdminRuntimeSettings, getBotNodes, getLLMMessages, updateAdminRuntimeSettings } from '../api'
|
||||
import type { AdminRuntimeSettings, BotNode, LLMMessage, ListResponse } from '../types'
|
||||
import { deleteLLMMessage, deleteLLMMessagesByBot, getBotNodes, getLLMMessages, updateBotNode } from '../api'
|
||||
import type { BotNode, LLMMessage, ListResponse } from '../types'
|
||||
|
||||
const loading = ref(false)
|
||||
const savingSettings = ref(false)
|
||||
const savingBotId = ref<number | null>(null)
|
||||
const error = ref('')
|
||||
const messages = ref<LLMMessage[]>([])
|
||||
const botNodes = ref<BotNode[]>([])
|
||||
@@ -13,7 +13,6 @@ const limit = 50
|
||||
const offset = ref(0)
|
||||
const selectedBotId = ref<number | ''>('')
|
||||
const includeDeleted = ref(false)
|
||||
const settings = ref<AdminRuntimeSettings | null>(null)
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
pending: 'background-color: #fff3cd;',
|
||||
@@ -29,51 +28,13 @@ const statusLabels: Record<string, string> = {
|
||||
error: '错误',
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const response = await getAdminRuntimeSettings()
|
||||
settings.value = response.item
|
||||
} catch (err) {
|
||||
console.error('加载设置失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleQueueEnabled() {
|
||||
if (!settings.value || savingSettings.value) return
|
||||
savingSettings.value = true
|
||||
try {
|
||||
await updateAdminRuntimeSettings({
|
||||
allow_encrypted_forwarding: settings.value.allow_encrypted_forwarding,
|
||||
llm_queue_enabled: !settings.value.llm_queue_enabled,
|
||||
llm_include_channel_messages: settings.value.llm_include_channel_messages,
|
||||
})
|
||||
settings.value.llm_queue_enabled = !settings.value.llm_queue_enabled
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
savingSettings.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleIncludeChannel() {
|
||||
if (!settings.value || savingSettings.value) return
|
||||
savingSettings.value = true
|
||||
try {
|
||||
await updateAdminRuntimeSettings({
|
||||
allow_encrypted_forwarding: settings.value.allow_encrypted_forwarding,
|
||||
llm_queue_enabled: settings.value.llm_queue_enabled,
|
||||
llm_include_channel_messages: !settings.value.llm_include_channel_messages,
|
||||
})
|
||||
settings.value.llm_include_channel_messages = !settings.value.llm_include_channel_messages
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
savingSettings.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function getMessageType(msg: LLMMessage): string {
|
||||
return msg.bot_id === 0 ? '频道' : '私聊'
|
||||
return msg.channel_id ? '频道' : '私聊'
|
||||
}
|
||||
|
||||
function getBotName(botId: number): string {
|
||||
const bot = botNodes.value.find(b => b.id === botId)
|
||||
return bot ? bot.long_name : '-'
|
||||
}
|
||||
|
||||
async function loadBotNodes() {
|
||||
@@ -85,6 +46,52 @@ async function loadBotNodes() {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleBotLLMQueue(bot: BotNode) {
|
||||
savingBotId.value = bot.id
|
||||
try {
|
||||
await updateBotNode(bot.id, {
|
||||
long_name: bot.long_name,
|
||||
short_name: bot.short_name,
|
||||
enabled: bot.enabled,
|
||||
default_channel_id: bot.default_channel_id,
|
||||
topic_prefix: bot.topic_prefix,
|
||||
psk: bot.psk,
|
||||
nodeinfo_broadcast_enabled: bot.nodeinfo_broadcast_enabled,
|
||||
nodeinfo_broadcast_interval_seconds: bot.nodeinfo_broadcast_interval_seconds,
|
||||
llm_queue_enabled: !bot.llm_queue_enabled,
|
||||
llm_include_channel_messages: bot.llm_include_channel_messages,
|
||||
})
|
||||
bot.llm_queue_enabled = !bot.llm_queue_enabled
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
savingBotId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleBotIncludeChannel(bot: BotNode) {
|
||||
savingBotId.value = bot.id
|
||||
try {
|
||||
await updateBotNode(bot.id, {
|
||||
long_name: bot.long_name,
|
||||
short_name: bot.short_name,
|
||||
enabled: bot.enabled,
|
||||
default_channel_id: bot.default_channel_id,
|
||||
topic_prefix: bot.topic_prefix,
|
||||
psk: bot.psk,
|
||||
nodeinfo_broadcast_enabled: bot.nodeinfo_broadcast_enabled,
|
||||
nodeinfo_broadcast_interval_seconds: bot.nodeinfo_broadcast_interval_seconds,
|
||||
llm_queue_enabled: bot.llm_queue_enabled,
|
||||
llm_include_channel_messages: !bot.llm_include_channel_messages,
|
||||
})
|
||||
bot.llm_include_channel_messages = !bot.llm_include_channel_messages
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
savingBotId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMessages(resetOffset = false) {
|
||||
if (resetOffset) {
|
||||
offset.value = 0
|
||||
@@ -165,7 +172,6 @@ function goNext() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSettings()
|
||||
loadBotNodes()
|
||||
loadMessages()
|
||||
})
|
||||
@@ -175,21 +181,42 @@ onMounted(() => {
|
||||
<div class="admin-llm">
|
||||
<h2>LLM 消息队列</h2>
|
||||
|
||||
<div class="admin-llm-section">
|
||||
<h3>机器人 LLM 设置</h3>
|
||||
<p class="section-desc">每个机器人可以独立启用或禁用 LLM 消息队列。启用后,该机器人收到的私聊消息将被加入队列。</p>
|
||||
|
||||
<div class="bot-settings-grid">
|
||||
<div v-for="bot in botNodes" :key="bot.id" class="bot-settings-card">
|
||||
<div class="bot-header">
|
||||
<strong>{{ bot.long_name }}</strong>
|
||||
<span class="bot-node-id">{{ bot.node_id }}</span>
|
||||
</div>
|
||||
<div class="bot-settings">
|
||||
<label class="setting-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="bot.llm_queue_enabled"
|
||||
@change="toggleBotLLMQueue(bot)"
|
||||
:disabled="savingBotId === bot.id"
|
||||
/>
|
||||
<span>启用 LLM 队列</span>
|
||||
</label>
|
||||
<label class="setting-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="bot.llm_include_channel_messages"
|
||||
@change="toggleBotIncludeChannel(bot)"
|
||||
:disabled="savingBotId === bot.id || !bot.llm_queue_enabled"
|
||||
/>
|
||||
<span>包含频道消息</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="savingBotId === bot.id" class="saving-indicator">保存中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-llm-toolbar">
|
||||
<div class="admin-llm-filter">
|
||||
<label>
|
||||
<input type="checkbox" :checked="settings?.llm_queue_enabled ?? false" @change="toggleQueueEnabled" :disabled="savingSettings" />
|
||||
启用 LLM 消息队列
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="admin-llm-filter">
|
||||
<label>
|
||||
<input type="checkbox" :checked="settings?.llm_include_channel_messages ?? false" @change="toggleIncludeChannel" :disabled="savingSettings || !settings?.llm_queue_enabled" />
|
||||
包含频道消息
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="admin-llm-filter">
|
||||
<label>选择机器人:</label>
|
||||
<select v-model="selectedBotId" @change="loadMessages(true)">
|
||||
@@ -226,6 +253,7 @@ onMounted(() => {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>机器人</th>
|
||||
<th>类型</th>
|
||||
<th>状态</th>
|
||||
<th>来自节点</th>
|
||||
@@ -239,6 +267,12 @@ onMounted(() => {
|
||||
<tbody>
|
||||
<tr v-for="msg in messages" :key="msg.id" :style="statusColors[msg.status]">
|
||||
<td>{{ msg.id }}</td>
|
||||
<td class="bot-name-cell">
|
||||
<div class="bot-info">
|
||||
<div class="bot-long-name">{{ getBotName(msg.bot_id) }}</div>
|
||||
<div class="bot-node-id" v-if="msg.bot_id !== 0">{{ msg.bot_node_id }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="type-badge" :class="getMessageType(msg) === '频道' ? 'channel' : 'direct'">
|
||||
{{ getMessageType(msg) }}
|
||||
@@ -255,7 +289,7 @@ onMounted(() => {
|
||||
<div class="node-id">{{ msg.from_node_id }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="channel-cell">{{ msg.channel_id || (msg.bot_id === 0 ? '默认频道' : '-') }}</td>
|
||||
<td class="channel-cell">{{ msg.channel_id || '-' }}</td>
|
||||
<td class="message-text">{{ msg.text }}</td>
|
||||
<td>{{ formatTime(msg.received_at) }}</td>
|
||||
<td>{{ formatTime(msg.processed_at) }}</td>
|
||||
@@ -266,7 +300,7 @@ onMounted(() => {
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="messages.length === 0">
|
||||
<td colspan="9" class="empty-state">暂无消息</td>
|
||||
<td colspan="10" class="empty-state">暂无消息</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -290,6 +324,76 @@ onMounted(() => {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.admin-llm-section {
|
||||
background: #f8f9fa;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.admin-llm-section h3 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
margin: 0 0 1rem;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.bot-settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.bot-settings-card {
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.bot-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.bot-node-id {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.bot-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.setting-item input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.saving-indicator {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
.admin-llm-toolbar {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
@@ -359,6 +463,26 @@ onMounted(() => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bot-name-cell {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.bot-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.bot-long-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.bot-node-id {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.admin-llm-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
@@ -530,6 +530,8 @@ export interface BotNode {
|
||||
nodeinfo_broadcast_enabled: boolean
|
||||
nodeinfo_broadcast_interval_seconds: number
|
||||
last_nodeinfo_broadcast_at: string | null
|
||||
llm_queue_enabled: boolean
|
||||
llm_include_channel_messages: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
@@ -544,6 +546,8 @@ export interface BotNodePayload {
|
||||
psk?: string
|
||||
nodeinfo_broadcast_enabled?: boolean
|
||||
nodeinfo_broadcast_interval_seconds?: number
|
||||
llm_queue_enabled?: boolean
|
||||
llm_include_channel_messages?: boolean
|
||||
}
|
||||
|
||||
export interface BotNodeMutationResponse {
|
||||
|
||||
Reference in New Issue
Block a user