完善私聊

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
+60 -8
View File
@@ -69,10 +69,33 @@
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -381,6 +404,40 @@
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
@@ -728,7 +785,6 @@
"integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -1339,7 +1395,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -1470,7 +1525,6 @@
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -1492,7 +1546,6 @@
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
@@ -1577,7 +1630,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.35.tgz",
"integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.35",
"@vue/compiler-sfc": "3.5.35",
@@ -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%); }