完善私聊

This commit is contained in:
2026-06-14 19:26:43 +08:00
parent a2d838d556
commit 5d4aced3e0
5 changed files with 650 additions and 34 deletions
@@ -7,13 +7,14 @@ const chatPageSize = 30
const maxTextBytes = 200
const topThreshold = 8
const bottomThreshold = 40
// 私聊固定走 PKIchannel_id 与固件 ServiceEnvelope 保持一致
const directChannelId = 'PKI'
const bots = ref<BotNode[]>([])
const targets = ref<NodeInfo[]>([])
const messages = ref<TextMessage[]>([])
const selectedBotId = ref<number | null>(null)
const selectedTargetId = ref('')
const channelId = ref('LongFast')
const text = ref('')
const loading = ref(false)
const sending = ref(false)
@@ -33,7 +34,7 @@ let restoreMessageCount = 0
const selectedBot = computed(() => bots.value.find((item) => item.id === selectedBotId.value) ?? null)
const selectedTarget = computed(() => targets.value.find((item) => item.node_id === selectedTargetId.value) ?? null)
const directTextBytes = computed(() => new TextEncoder().encode(text.value).length)
const canSend = computed(() => !!selectedBot.value && !!selectedTarget.value && !!channelId.value.trim() && !!text.value.trim() && directTextBytes.value <= maxTextBytes && !sending.value)
const canSend = computed(() => !!selectedBot.value && !!selectedTarget.value && !!text.value.trim() && directTextBytes.value <= maxTextBytes && !sending.value)
const groupedMessages = computed(() => {
const groups = new Map<string, TextMessage & { mergedCount: number; mergedMessages: TextMessage[] }>()
for (const item of messages.value) {
@@ -49,8 +50,7 @@ const groupedMessages = computed(() => {
return Array.from(groups.values())
})
watch(selectedBot, (bot) => {
if (bot) channelId.value = bot.default_channel_id
watch(selectedBot, () => {
resetChat()
loadInitialMessages()
})
@@ -60,11 +60,6 @@ watch(selectedTargetId, () => {
loadInitialMessages()
})
watch(channelId, () => {
resetChat()
loadInitialMessages()
})
function resetChat() {
messages.value = []
hasMore.value = true
@@ -110,7 +105,6 @@ async function refreshLists() {
targets.value = nodeResponse.items
if (!selectedBotId.value && bots.value.length > 0) {
selectedBotId.value = bots.value[0].id
channelId.value = bots.value[0].default_channel_id
}
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
@@ -123,7 +117,7 @@ async function loadInitialMessages() {
if (!selectedBot.value || !selectedTarget.value) return
loadingOlder.value = true
try {
const response = await getBotDirectTextMessages(selectedBot.value.id, selectedTarget.value.node_num, chatPageSize, 0, channelId.value)
const response = await getBotDirectTextMessages(selectedBot.value.id, selectedTarget.value.node_num, chatPageSize, 0, directChannelId)
messages.value = toChronological(response.items)
hasMore.value = response.items.length === chatPageSize
initialized.value = true
@@ -141,7 +135,7 @@ async function loadOlderMessages() {
if (!selectedBot.value || !selectedTarget.value || loadingOlder.value || !hasMore.value) return
loadingOlder.value = true
try {
const response = await getBotDirectTextMessages(selectedBot.value.id, selectedTarget.value.node_num, chatPageSize, messages.value.length, channelId.value)
const response = await getBotDirectTextMessages(selectedBot.value.id, selectedTarget.value.node_num, chatPageSize, messages.value.length, directChannelId)
messages.value = mergeMessages(messages.value, toChronological(response.items))
hasMore.value = response.items.length === chatPageSize
} catch (err) {
@@ -153,7 +147,7 @@ async function loadOlderMessages() {
async function pollLatestMessages() {
if (!selectedBot.value || !selectedTarget.value) return
const response = await getBotDirectTextMessages(selectedBot.value.id, selectedTarget.value.node_num, chatPageSize, 0, channelId.value)
const response = await getBotDirectTextMessages(selectedBot.value.id, selectedTarget.value.node_num, chatPageSize, 0, directChannelId)
messages.value = mergeMessages(messages.value, toChronological(response.items))
}
@@ -163,7 +157,7 @@ async function sendDirectMessage() {
error.value = ''
notice.value = ''
try {
const response = await sendBotMessage({ bot_id: selectedBot.value.id, message_type: 'direct', channel_id: channelId.value, to_node_id: selectedTarget.value.node_id, text: text.value })
const response = await sendBotMessage({ bot_id: selectedBot.value.id, message_type: 'direct', channel_id: directChannelId, to_node_id: selectedTarget.value.node_id, text: text.value })
if (response.error) {
error.value = response.error
} else {
@@ -240,7 +234,7 @@ onBeforeUnmount(() => {
<div class="direct-header">
<div>
<p class="eyebrow">Direct Bot Chat</p>
<h2>机器人私聊功能未完成</h2>
<h2>机器人私聊 <span class="pki-badge" title="使用 X25519 + AES-CCM 与目标节点端到端加密">PKI 加密</span></h2>
</div>
<div class="direct-actions">
<a class="admin-button secondary" href="/admin/bot">返回频道聊天</a>
@@ -264,9 +258,10 @@ onBeforeUnmount(() => {
<option v-for="node in targets" :key="node.node_id" :value="node.node_id">{{ node.long_name || node.short_name || node.node_id }} · {{ node.node_id }}</option>
</select>
</label>
<label>频道 ID<input v-model="channelId" /></label>
</div>
<p class="direct-hint">私聊固定走 PKIchannel_id = "PKI"需要目标节点已上报 NodeInfo 公钥才能加密</p>
<div ref="panelRef" class="direct-chat-list" @scroll.passive="handleScroll">
<div v-if="loadingOlder" class="chat-loading">正在加载更早消息...</div>
<div v-else-if="!hasMore && messages.length > 0" class="chat-end">没有更多历史消息</div>
@@ -293,7 +288,9 @@ onBeforeUnmount(() => {
<style scoped>
.direct-page { display: grid; gap: 12px; padding: 16px; }
.direct-header, .direct-actions, .send-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.direct-selectors { display: grid; grid-template-columns: repeat(3, minmax(180px, 1fr)); gap: 12px; }
.direct-selectors { display: grid; grid-template-columns: repeat(2, minmax(180px, 1fr)); gap: 12px; }
.direct-hint { color: #475569; font-size: 12px; margin: 0; }
.pki-badge { display: inline-flex; align-items: center; margin-left: 8px; border-radius: 999px; padding: 2px 10px; color: #1d4ed8; background: #dbeafe; font-size: 12px; font-weight: 700; vertical-align: middle; }
label { display: grid; gap: 5px; color: #334155; font-size: 13px; font-weight: 800; }
input, select, textarea { box-sizing: border-box; width: 100%; border: 1px solid #cbd5e1; border-radius: 10px; padding: 9px 11px; color: #0f172a; font: inherit; background: #fff; }
.direct-chat-list { min-height: 420px; max-height: 560px; overflow: auto; display: flex; flex-direction: column; gap: 10px; border: 1px solid #e2e8f0; border-radius: 14px; padding: 14px; background: linear-gradient(180deg, #f8fafc 0%, #eef4ff 100%); }