完善私聊
This commit is contained in:
Generated
+60
-8
@@ -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
|
||||
// 私聊固定走 PKI,channel_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">私聊固定走 PKI(channel_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%); }
|
||||
|
||||
Reference in New Issue
Block a user