2026-04-29 12:33:26 +08:00
|
|
|
<template>
|
|
|
|
|
<div v-if="app">
|
|
|
|
|
<el-page-header @back="$router.back()" content="IM 回调配置" style="margin-bottom:24px" />
|
|
|
|
|
|
|
|
|
|
<el-card style="margin-bottom:16px">
|
|
|
|
|
<el-descriptions :column="2" border>
|
|
|
|
|
<el-descriptions-item label="应用名称">{{ app.name }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="包名">{{ app.packageName }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="AppKey">
|
|
|
|
|
<el-text class="mono">{{ app.appKey }}</el-text>
|
|
|
|
|
<el-button link @click="copy(app.appKey)">
|
|
|
|
|
<el-icon><CopyDocument /></el-icon>
|
|
|
|
|
</el-button>
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="说明">
|
|
|
|
|
只管理当前租户 IM 服务的回调地址和签名密钥。
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<el-card style="margin-bottom:16px">
|
|
|
|
|
<template #header>接入说明</template>
|
|
|
|
|
<el-alert
|
|
|
|
|
title="回调配置只针对当前租户的 IM 服务,不包含用户关系、黑名单、好友申请等用户域操作。"
|
|
|
|
|
type="info"
|
|
|
|
|
:closable="false"
|
|
|
|
|
show-icon
|
|
|
|
|
style="margin-bottom:16px"
|
|
|
|
|
/>
|
|
|
|
|
<el-descriptions :column="1" border>
|
|
|
|
|
<el-descriptions-item label="调用方式">服务端以 `POST` 方式推送到你配置的回调地址。</el-descriptions-item>
|
2026-05-08 18:32:00 +08:00
|
|
|
<el-descriptions-item label="签名头">`X-App-Key`、`X-App-Timestamp`、`X-App-Nonce`、`X-App-Signature`。</el-descriptions-item>
|
2026-05-07 19:39:47 +08:00
|
|
|
<el-descriptions-item label="验签公式">`HMAC-SHA256(appSecret, appKey + '\\n' + timestamp + '\\n' + nonce + '\\n' + sha256(body))`。</el-descriptions-item>
|
2026-04-29 12:33:26 +08:00
|
|
|
<el-descriptions-item label="幂等建议">接收方建议按 `callbackId` 去重。</el-descriptions-item>
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
<el-table :data="webhookEvents" border stripe style="margin-top:16px">
|
|
|
|
|
<el-table-column prop="event" label="事件" width="180" />
|
|
|
|
|
<el-table-column prop="payload" label="payload" width="220" />
|
|
|
|
|
<el-table-column prop="description" label="说明" min-width="320" />
|
|
|
|
|
</el-table>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
2026-05-02 22:57:55 +08:00
|
|
|
<el-card style="margin-bottom:16px">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="toolbar toolbar-space-between">
|
|
|
|
|
<span>回调地址</span>
|
2026-05-07 19:39:47 +08:00
|
|
|
<el-button link type="primary" @click="$router.push({ path: `/apps/${appKey}/im-webhook-alerts` })">
|
2026-05-02 22:57:55 +08:00
|
|
|
查看告警
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
2026-04-29 12:33:26 +08:00
|
|
|
<div class="toolbar toolbar-space-between">
|
|
|
|
|
<el-button type="primary" @click="openCreateWebhookDialog">新增回调</el-button>
|
|
|
|
|
<el-button @click="loadWebhooks" :loading="loadingWebhooks">刷新</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-table :data="webhooks" v-loading="loadingWebhooks" border stripe>
|
|
|
|
|
<el-table-column prop="url" label="回调地址" min-width="260" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="secret" label="密钥" width="120">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
{{ row.secret ? '******' : '-' }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
2026-05-02 22:57:55 +08:00
|
|
|
<el-table-column prop="enabled" label="启用" width="90">
|
2026-04-29 12:33:26 +08:00
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-tag :type="row.enabled ? 'success' : 'info'" size="small">
|
|
|
|
|
{{ row.enabled ? '启用' : '停用' }}
|
|
|
|
|
</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
2026-05-02 22:57:55 +08:00
|
|
|
<el-table-column label="健康" width="120">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-tag
|
|
|
|
|
v-if="row.consecutiveFailures && row.consecutiveFailures > 0"
|
|
|
|
|
:type="row.consecutiveFailures >= 10 ? 'danger' : 'warning'"
|
|
|
|
|
size="small"
|
|
|
|
|
>
|
|
|
|
|
失败 {{ row.consecutiveFailures }} 次
|
|
|
|
|
</el-tag>
|
|
|
|
|
<el-tag v-else type="success" size="small">正常</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="createdAt" label="创建时间" width="170">
|
2026-04-29 12:33:26 +08:00
|
|
|
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
|
|
|
</el-table-column>
|
2026-05-02 22:57:55 +08:00
|
|
|
<el-table-column label="操作" width="220" fixed="right">
|
2026-04-29 12:33:26 +08:00
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-button link type="primary" size="small" @click="openEditWebhookDialog(row)">编辑</el-button>
|
2026-05-07 19:39:47 +08:00
|
|
|
<el-button link type="info" size="small" @click="$router.push({ path: `/apps/${appKey}/im-webhooks/${row.id}/deliveries` })">日志</el-button>
|
2026-04-29 12:33:26 +08:00
|
|
|
<el-button link type="danger" size="small" @click="deleteWebhook(row)">删除</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
v-model="showWebhookDialog"
|
|
|
|
|
:title="editingWebhookId ? '编辑回调配置' : '新增回调配置'"
|
|
|
|
|
width="520px"
|
|
|
|
|
@closed="resetWebhookForm"
|
|
|
|
|
>
|
|
|
|
|
<el-form :model="webhookForm" label-width="90px">
|
|
|
|
|
<el-form-item label="回调地址">
|
|
|
|
|
<el-input v-model="webhookForm.url" placeholder="https://example.com/webhook" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="回调密钥">
|
|
|
|
|
<el-input v-model="webhookForm.secret" placeholder="留空则不修改" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="启用">
|
|
|
|
|
<el-switch v-model="webhookForm.enabled" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<el-button @click="showWebhookDialog = false">取消</el-button>
|
|
|
|
|
<el-button type="primary" :loading="submittingWebhook" @click="submitWebhookForm">保存</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, onMounted, ref } from 'vue'
|
|
|
|
|
import { useRoute } from 'vue-router'
|
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
import { CopyDocument } from '@element-plus/icons-vue'
|
|
|
|
|
import { appApi, type App } from '@/api/app'
|
|
|
|
|
import { imAdminApi, type WebhookConfig, type WebhookConfigForm } from '@/api/im'
|
2026-05-21 16:09:55 +08:00
|
|
|
import { formatTime } from '@/utils/date'
|
2026-04-29 12:33:26 +08:00
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
const app = ref<App | null>(null)
|
|
|
|
|
const webhooks = ref<WebhookConfig[]>([])
|
|
|
|
|
const loadingWebhooks = ref(false)
|
|
|
|
|
const showWebhookDialog = ref(false)
|
|
|
|
|
const submittingWebhook = ref(false)
|
|
|
|
|
const editingWebhookId = ref<string | null>(null)
|
|
|
|
|
const webhookForm = ref<WebhookConfigForm & { enabled: boolean }>({
|
|
|
|
|
url: '',
|
|
|
|
|
secret: '',
|
|
|
|
|
enabled: true,
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-07 19:39:47 +08:00
|
|
|
const appKey = computed(() => route.params.appKey as string)
|
2026-04-29 12:33:26 +08:00
|
|
|
|
|
|
|
|
const webhookEvents = [
|
|
|
|
|
{ event: 'message.sent', payload: 'ImMessageEntity', description: '消息发送成功后触发。' },
|
|
|
|
|
{ event: 'message.revoked', payload: 'ImMessageEntity', description: '消息撤回后触发。' },
|
|
|
|
|
{ event: 'message.edited', payload: 'ImMessageEntity', description: '文本消息编辑后触发。' },
|
|
|
|
|
{ event: 'message.read', payload: 'MessageReadCallbackPayload', description: '已读回执同步后触发。' },
|
|
|
|
|
{ event: 'friend.request.sent', payload: 'FriendRequestCallbackPayload', description: '好友申请创建后触发。' },
|
|
|
|
|
{ event: 'friend.request.accepted', payload: 'FriendRequestCallbackPayload', description: '好友申请通过后触发。' },
|
|
|
|
|
{ event: 'friend.request.rejected', payload: 'FriendRequestCallbackPayload', description: '好友申请拒绝后触发。' },
|
|
|
|
|
{ event: 'group.join.request.sent', payload: 'GroupJoinRequestCallbackPayload', description: '入群申请创建后触发。' },
|
|
|
|
|
{ event: 'group.join.request.accepted', payload: 'GroupJoinRequestCallbackPayload', description: '入群申请通过后触发。' },
|
|
|
|
|
{ event: 'group.join.request.rejected', payload: 'GroupJoinRequestCallbackPayload', description: '入群申请拒绝后触发。' },
|
|
|
|
|
{ event: 'blacklist.added', payload: 'BlacklistCallbackPayload', description: '黑名单记录新增后触发。' },
|
|
|
|
|
{ event: 'blacklist.removed', payload: 'BlacklistCallbackPayload', description: '黑名单记录移除后触发。' },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
async function loadApp() {
|
2026-05-07 19:39:47 +08:00
|
|
|
const res = await appApi.get(appKey.value)
|
2026-04-29 12:33:26 +08:00
|
|
|
app.value = res.data.data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadWebhooks() {
|
|
|
|
|
loadingWebhooks.value = true
|
|
|
|
|
try {
|
2026-05-07 19:39:47 +08:00
|
|
|
const res = await imAdminApi.listWebhooks(appKey.value)
|
2026-04-29 12:33:26 +08:00
|
|
|
webhooks.value = res.data.data
|
|
|
|
|
} finally {
|
|
|
|
|
loadingWebhooks.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openCreateWebhookDialog() {
|
|
|
|
|
editingWebhookId.value = null
|
|
|
|
|
webhookForm.value = { url: '', secret: '', enabled: true }
|
|
|
|
|
showWebhookDialog.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openEditWebhookDialog(row: WebhookConfig) {
|
|
|
|
|
editingWebhookId.value = row.id
|
|
|
|
|
webhookForm.value = {
|
|
|
|
|
url: row.url,
|
|
|
|
|
secret: '',
|
|
|
|
|
enabled: row.enabled,
|
|
|
|
|
}
|
|
|
|
|
showWebhookDialog.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetWebhookForm() {
|
|
|
|
|
editingWebhookId.value = null
|
|
|
|
|
webhookForm.value = { url: '', secret: '', enabled: true }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function submitWebhookForm() {
|
|
|
|
|
if (!webhookForm.value.url.trim()) {
|
|
|
|
|
ElMessage.warning('请填写回调地址')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
submittingWebhook.value = true
|
|
|
|
|
try {
|
|
|
|
|
const payload: WebhookConfigForm & { enabled: boolean } = {
|
|
|
|
|
url: webhookForm.value.url.trim(),
|
|
|
|
|
enabled: webhookForm.value.enabled,
|
|
|
|
|
}
|
|
|
|
|
const secret = webhookForm.value.secret.trim()
|
|
|
|
|
if (secret) {
|
|
|
|
|
payload.secret = secret
|
|
|
|
|
}
|
|
|
|
|
if (editingWebhookId.value) {
|
2026-05-07 19:39:47 +08:00
|
|
|
await imAdminApi.updateWebhook(appKey.value, editingWebhookId.value, payload)
|
2026-04-29 12:33:26 +08:00
|
|
|
} else {
|
2026-05-07 19:39:47 +08:00
|
|
|
await imAdminApi.createWebhook(appKey.value, payload)
|
2026-04-29 12:33:26 +08:00
|
|
|
}
|
|
|
|
|
ElMessage.success('回调配置已保存')
|
|
|
|
|
showWebhookDialog.value = false
|
|
|
|
|
await loadWebhooks()
|
|
|
|
|
} finally {
|
|
|
|
|
submittingWebhook.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deleteWebhook(row: WebhookConfig) {
|
|
|
|
|
await ElMessageBox.confirm(`确认删除回调配置 ${row.url}?`, '删除回调', {
|
|
|
|
|
type: 'warning',
|
|
|
|
|
confirmButtonText: '确认删除',
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
})
|
2026-05-07 19:39:47 +08:00
|
|
|
await imAdminApi.deleteWebhook(appKey.value, row.id)
|
2026-04-29 12:33:26 +08:00
|
|
|
ElMessage.success('已删除')
|
|
|
|
|
await loadWebhooks()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function copy(text: string) {
|
|
|
|
|
navigator.clipboard.writeText(text)
|
|
|
|
|
ElMessage.success('已复制')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
await Promise.all([loadApp(), loadWebhooks()])
|
|
|
|
|
})
|
|
|
|
|
</script>
|