mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
feat(acl): source-aware chat trigger ACL (#252)
This commit is contained in:
@@ -27,9 +27,9 @@ export const ModelConfigModel = z.object({
|
|||||||
|
|
||||||
export const IdentityContextModel = z.object({
|
export const IdentityContextModel = z.object({
|
||||||
botId: z.string().min(1, 'Bot ID is required'),
|
botId: z.string().min(1, 'Bot ID is required'),
|
||||||
containerId: z.string().min(1, 'Container ID is required'),
|
containerId: z.string().optional().default(''),
|
||||||
channelIdentityId: z.string().min(1, 'Channel identity ID is required'),
|
channelIdentityId: z.string().optional().default(''),
|
||||||
displayName: z.string().min(1, 'Display name is required'),
|
displayName: z.string().optional().default(''),
|
||||||
currentPlatform: z.string().optional(),
|
currentPlatform: z.string().optional(),
|
||||||
replyTarget: z.string().optional(),
|
replyTarget: z.string().optional(),
|
||||||
conversationType: z.string().optional(),
|
conversationType: z.string().optional(),
|
||||||
|
|||||||
@@ -501,6 +501,11 @@
|
|||||||
"editAvatarDescription": "Set the image URL for the bot avatar.",
|
"editAvatarDescription": "Set the image URL for the bot avatar.",
|
||||||
"avatarUpdateSuccess": "Avatar updated",
|
"avatarUpdateSuccess": "Avatar updated",
|
||||||
"avatarUpdateFailed": "Failed to update avatar",
|
"avatarUpdateFailed": "Failed to update avatar",
|
||||||
|
"typePlaceholder": "Select bot type",
|
||||||
|
"types": {
|
||||||
|
"personal": "Personal",
|
||||||
|
"public": "Public"
|
||||||
|
},
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
"inactive": "Inactive",
|
"inactive": "Inactive",
|
||||||
"lifecycle": {
|
"lifecycle": {
|
||||||
@@ -753,6 +758,7 @@
|
|||||||
"browserContext": "Browser Context",
|
"browserContext": "Browser Context",
|
||||||
"browserContextPlaceholder": "Select browser context (disabled if empty)",
|
"browserContextPlaceholder": "Select browser context (disabled if empty)",
|
||||||
"allowGuest": "Allow Guest Access",
|
"allowGuest": "Allow Guest Access",
|
||||||
|
"allowGuestPersonalHint": "Personal bots do not support guest access. Use a public bot instead.",
|
||||||
"loopDetectionTitle": "Detect and auto-block output loops",
|
"loopDetectionTitle": "Detect and auto-block output loops",
|
||||||
"searchModel": "Search models…",
|
"searchModel": "Search models…",
|
||||||
"noModel": "No models available",
|
"noModel": "No models available",
|
||||||
@@ -793,7 +799,9 @@
|
|||||||
"blacklistEmpty": "No blacklist rules yet",
|
"blacklistEmpty": "No blacklist rules yet",
|
||||||
"validation": "Fill exactly one subject: user_id or channel_identity_id",
|
"validation": "Fill exactly one subject: user_id or channel_identity_id",
|
||||||
"validationConversationRequiresChannel": "Select a source platform before restricting by conversation or thread ID",
|
"validationConversationRequiresChannel": "Select a source platform before restricting by conversation or thread ID",
|
||||||
|
"validationConversationRequiresGroupOrThread": "Conversation ID restrictions are only supported for group or thread rules",
|
||||||
"validationThreadRequiresConversation": "Thread ID requires a conversation ID",
|
"validationThreadRequiresConversation": "Thread ID requires a conversation ID",
|
||||||
|
"validationThreadRequiresThreadType": "Thread ID restrictions require conversation type to be Thread",
|
||||||
"whitelistSaved": "Whitelist updated",
|
"whitelistSaved": "Whitelist updated",
|
||||||
"blacklistSaved": "Blacklist updated",
|
"blacklistSaved": "Blacklist updated",
|
||||||
"saveFailed": "Failed to save access rule",
|
"saveFailed": "Failed to save access rule",
|
||||||
@@ -808,12 +816,12 @@
|
|||||||
"privateConversationType": "Private",
|
"privateConversationType": "Private",
|
||||||
"groupConversationType": "Group",
|
"groupConversationType": "Group",
|
||||||
"threadConversationType": "Thread",
|
"threadConversationType": "Thread",
|
||||||
"observedConversation": "Observed Conversation",
|
"observedConversation": "Observed Group Or Thread",
|
||||||
"selectObservedConversation": "Select an observed conversation",
|
"selectObservedConversation": "Select an observed group or thread",
|
||||||
"searchObservedConversation": "Search observed conversations",
|
"searchObservedConversation": "Search observed groups or threads",
|
||||||
"noObservedConversations": "No observed conversations",
|
"noObservedConversations": "No observed groups or threads",
|
||||||
"selectIdentityFirst": "Select a platform identity first to load observed conversations",
|
"selectIdentityFirst": "Select a platform identity first to load observed groups or threads",
|
||||||
"observedConversationHint": "Choosing an observed conversation auto-fills the platform, conversation ID, and thread ID.",
|
"observedConversationHint": "Use this only for group or thread rules. Private access should be controlled by conversation type alone.",
|
||||||
"conversationId": "Conversation ID",
|
"conversationId": "Conversation ID",
|
||||||
"conversationIdPlaceholder": "Enter conversation ID",
|
"conversationIdPlaceholder": "Enter conversation ID",
|
||||||
"threadId": "Thread ID",
|
"threadId": "Thread ID",
|
||||||
@@ -847,8 +855,6 @@
|
|||||||
"webhookCallback": "WebHook Callback URL",
|
"webhookCallback": "WebHook Callback URL",
|
||||||
"webhookCallbackHint": "Use this URL as the event subscription request URL in Feishu/Lark.",
|
"webhookCallbackHint": "Use this URL as the event subscription request URL in Feishu/Lark.",
|
||||||
"webhookCallbackPending": "Save this platform configuration to generate the callback URL.",
|
"webhookCallbackPending": "Save this platform configuration to generate the callback URL.",
|
||||||
"feishuWebhookSecurityHint": "For security, webhook mode requires either an Encrypt Key or a Verification Token; an unprotected public callback URL should not be exposed.",
|
|
||||||
"feishuWebhookSecretRequired": "For security, configure at least one of Encrypt Key or Verification Token.",
|
|
||||||
"noAvailableTypes": "All platform types have been configured",
|
"noAvailableTypes": "All platform types have been configured",
|
||||||
"types": {
|
"types": {
|
||||||
"feishu": "Feishu",
|
"feishu": "Feishu",
|
||||||
|
|||||||
@@ -497,6 +497,11 @@
|
|||||||
"editAvatarDescription": "设置 Bot 头像图片的链接地址",
|
"editAvatarDescription": "设置 Bot 头像图片的链接地址",
|
||||||
"avatarUpdateSuccess": "头像已更新",
|
"avatarUpdateSuccess": "头像已更新",
|
||||||
"avatarUpdateFailed": "更新头像失败",
|
"avatarUpdateFailed": "更新头像失败",
|
||||||
|
"typePlaceholder": "选择 Bot 类型",
|
||||||
|
"types": {
|
||||||
|
"personal": "个人",
|
||||||
|
"public": "公开"
|
||||||
|
},
|
||||||
"active": "运行中",
|
"active": "运行中",
|
||||||
"inactive": "未启用",
|
"inactive": "未启用",
|
||||||
"lifecycle": {
|
"lifecycle": {
|
||||||
@@ -749,6 +754,7 @@
|
|||||||
"browserContext": "浏览器上下文",
|
"browserContext": "浏览器上下文",
|
||||||
"browserContextPlaceholder": "选择浏览器上下文(未配置时不启用)",
|
"browserContextPlaceholder": "选择浏览器上下文(未配置时不启用)",
|
||||||
"allowGuest": "允许游客访问",
|
"allowGuest": "允许游客访问",
|
||||||
|
"allowGuestPersonalHint": "个人 Bot 不支持游客访问,请使用公开 Bot。",
|
||||||
"loopDetectionTitle": "自动检测并阻止模型循环输出",
|
"loopDetectionTitle": "自动检测并阻止模型循环输出",
|
||||||
"searchModel": "搜索模型…",
|
"searchModel": "搜索模型…",
|
||||||
"noModel": "暂无可选模型",
|
"noModel": "暂无可选模型",
|
||||||
@@ -789,7 +795,9 @@
|
|||||||
"blacklistEmpty": "暂无黑名单规则",
|
"blacklistEmpty": "暂无黑名单规则",
|
||||||
"validation": "请只填写一个主体:user_id 或 channel_identity_id",
|
"validation": "请只填写一个主体:user_id 或 channel_identity_id",
|
||||||
"validationConversationRequiresChannel": "按会话或线程限制时,请先选择来源平台",
|
"validationConversationRequiresChannel": "按会话或线程限制时,请先选择来源平台",
|
||||||
|
"validationConversationRequiresGroupOrThread": "会话 ID 只支持用于群聊或线程规则",
|
||||||
"validationThreadRequiresConversation": "填写线程 ID 前必须先填写会话 ID",
|
"validationThreadRequiresConversation": "填写线程 ID 前必须先填写会话 ID",
|
||||||
|
"validationThreadRequiresThreadType": "线程 ID 只支持用于线程规则",
|
||||||
"whitelistSaved": "白名单已更新",
|
"whitelistSaved": "白名单已更新",
|
||||||
"blacklistSaved": "黑名单已更新",
|
"blacklistSaved": "黑名单已更新",
|
||||||
"saveFailed": "保存访问规则失败",
|
"saveFailed": "保存访问规则失败",
|
||||||
@@ -804,12 +812,12 @@
|
|||||||
"privateConversationType": "私聊",
|
"privateConversationType": "私聊",
|
||||||
"groupConversationType": "群聊",
|
"groupConversationType": "群聊",
|
||||||
"threadConversationType": "线程",
|
"threadConversationType": "线程",
|
||||||
"observedConversation": "历史会话",
|
"observedConversation": "历史群聊或线程",
|
||||||
"selectObservedConversation": "选择一个历史会话",
|
"selectObservedConversation": "选择一个历史群聊或线程",
|
||||||
"searchObservedConversation": "搜索历史会话",
|
"searchObservedConversation": "搜索历史群聊或线程",
|
||||||
"noObservedConversations": "暂无历史会话",
|
"noObservedConversations": "暂无历史群聊或线程",
|
||||||
"selectIdentityFirst": "请先选择平台身份以加载历史会话",
|
"selectIdentityFirst": "请先选择平台身份以加载历史群聊或线程",
|
||||||
"observedConversationHint": "选择历史会话后会自动填充平台、会话 ID 和线程 ID。",
|
"observedConversationHint": "这里只用于群聊或线程规则。私聊访问只需要通过会话类型控制。",
|
||||||
"conversationId": "会话 ID",
|
"conversationId": "会话 ID",
|
||||||
"conversationIdPlaceholder": "输入会话 ID",
|
"conversationIdPlaceholder": "输入会话 ID",
|
||||||
"threadId": "线程 ID",
|
"threadId": "线程 ID",
|
||||||
@@ -843,8 +851,6 @@
|
|||||||
"webhookCallback": "WebHook 回调地址",
|
"webhookCallback": "WebHook 回调地址",
|
||||||
"webhookCallbackHint": "将该地址配置到飞书/Lark 事件订阅的请求 URL。",
|
"webhookCallbackHint": "将该地址配置到飞书/Lark 事件订阅的请求 URL。",
|
||||||
"webhookCallbackPending": "保存平台配置后会生成回调地址。",
|
"webhookCallbackPending": "保存平台配置后会生成回调地址。",
|
||||||
"feishuWebhookSecurityHint": "出于安全考虑,Webhook 模式必须配置 Encrypt Key 或 Verification Token 之一;未受保护的回调地址不应直接暴露在公网上。",
|
|
||||||
"feishuWebhookSecretRequired": "出于安全考虑,请至少配置 Encrypt Key 或 Verification Token 之一。",
|
|
||||||
"noAvailableTypes": "所有平台类型均已配置",
|
"noAvailableTypes": "所有平台类型均已配置",
|
||||||
"types": {
|
"types": {
|
||||||
"feishu": "飞书",
|
"feishu": "飞书",
|
||||||
|
|||||||
@@ -18,16 +18,22 @@
|
|||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
{{ $t('bots.access.allowGuestDescription') }}
|
{{ $t('bots.access.allowGuestDescription') }}
|
||||||
</p>
|
</p>
|
||||||
|
<p
|
||||||
|
v-if="isPersonalBot"
|
||||||
|
class="text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
{{ $t('bots.settings.allowGuestPersonalHint') }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
:model-value="allowGuestDraft"
|
:model-value="allowGuestDraft"
|
||||||
:disabled="isSavingGuestAccess"
|
:disabled="isPersonalBot || isSavingGuestAccess"
|
||||||
@update:model-value="(val) => allowGuestDraft = !!val"
|
@update:model-value="(val) => allowGuestDraft = !!val"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
:disabled="!hasGuestAccessChanges || isSavingGuestAccess"
|
:disabled="isPersonalBot || !hasGuestAccessChanges || isSavingGuestAccess"
|
||||||
@click="handleSaveGuestAccess"
|
@click="handleSaveGuestAccess"
|
||||||
>
|
>
|
||||||
<Spinner
|
<Spinner
|
||||||
@@ -159,6 +165,7 @@
|
|||||||
<NativeSelect
|
<NativeSelect
|
||||||
v-else
|
v-else
|
||||||
v-model="whitelistSelection.sourceChannel"
|
v-model="whitelistSelection.sourceChannel"
|
||||||
|
:disabled="isObservedConversationLocked(whitelistSelection)"
|
||||||
class="h-9 w-full text-sm"
|
class="h-9 w-full text-sm"
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">
|
||||||
@@ -178,6 +185,7 @@
|
|||||||
<Label>{{ $t('bots.access.conversationType') }}</Label>
|
<Label>{{ $t('bots.access.conversationType') }}</Label>
|
||||||
<NativeSelect
|
<NativeSelect
|
||||||
v-model="whitelistSelection.sourceConversationType"
|
v-model="whitelistSelection.sourceConversationType"
|
||||||
|
:disabled="isObservedConversationLocked(whitelistSelection)"
|
||||||
class="h-9 w-full text-sm"
|
class="h-9 w-full text-sm"
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">
|
||||||
@@ -195,7 +203,10 @@
|
|||||||
</NativeSelect>
|
</NativeSelect>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2 md:col-span-2">
|
<div
|
||||||
|
v-if="whitelistSelection.sourceConversationType !== 'private'"
|
||||||
|
class="space-y-2 md:col-span-2"
|
||||||
|
>
|
||||||
<Label>{{ $t('bots.access.observedConversation') }}</Label>
|
<Label>{{ $t('bots.access.observedConversation') }}</Label>
|
||||||
<SearchableSelectPopover
|
<SearchableSelectPopover
|
||||||
v-if="whitelistSelection.channelIdentityId"
|
v-if="whitelistSelection.channelIdentityId"
|
||||||
@@ -223,18 +234,26 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div
|
||||||
|
v-if="whitelistSelection.sourceConversationType !== 'private'"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
<Label>{{ $t('bots.access.conversationId') }}</Label>
|
<Label>{{ $t('bots.access.conversationId') }}</Label>
|
||||||
<Input
|
<Input
|
||||||
v-model="whitelistSelection.sourceConversationId"
|
v-model="whitelistSelection.sourceConversationId"
|
||||||
|
:disabled="isObservedConversationLocked(whitelistSelection)"
|
||||||
:placeholder="$t('bots.access.conversationIdPlaceholder')"
|
:placeholder="$t('bots.access.conversationIdPlaceholder')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div
|
||||||
|
v-if="whitelistSelection.sourceConversationType !== 'private'"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
<Label>{{ $t('bots.access.threadId') }}</Label>
|
<Label>{{ $t('bots.access.threadId') }}</Label>
|
||||||
<Input
|
<Input
|
||||||
v-model="whitelistSelection.sourceThreadId"
|
v-model="whitelistSelection.sourceThreadId"
|
||||||
|
:disabled="isObservedConversationLocked(whitelistSelection)"
|
||||||
:placeholder="$t('bots.access.threadIdPlaceholder')"
|
:placeholder="$t('bots.access.threadIdPlaceholder')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -441,6 +460,7 @@
|
|||||||
<NativeSelect
|
<NativeSelect
|
||||||
v-else
|
v-else
|
||||||
v-model="blacklistSelection.sourceChannel"
|
v-model="blacklistSelection.sourceChannel"
|
||||||
|
:disabled="isObservedConversationLocked(blacklistSelection)"
|
||||||
class="h-9 w-full text-sm"
|
class="h-9 w-full text-sm"
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">
|
||||||
@@ -460,6 +480,7 @@
|
|||||||
<Label>{{ $t('bots.access.conversationType') }}</Label>
|
<Label>{{ $t('bots.access.conversationType') }}</Label>
|
||||||
<NativeSelect
|
<NativeSelect
|
||||||
v-model="blacklistSelection.sourceConversationType"
|
v-model="blacklistSelection.sourceConversationType"
|
||||||
|
:disabled="isObservedConversationLocked(blacklistSelection)"
|
||||||
class="h-9 w-full text-sm"
|
class="h-9 w-full text-sm"
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">
|
||||||
@@ -477,7 +498,10 @@
|
|||||||
</NativeSelect>
|
</NativeSelect>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2 md:col-span-2">
|
<div
|
||||||
|
v-if="blacklistSelection.sourceConversationType !== 'private'"
|
||||||
|
class="space-y-2 md:col-span-2"
|
||||||
|
>
|
||||||
<Label>{{ $t('bots.access.observedConversation') }}</Label>
|
<Label>{{ $t('bots.access.observedConversation') }}</Label>
|
||||||
<SearchableSelectPopover
|
<SearchableSelectPopover
|
||||||
v-if="blacklistSelection.channelIdentityId"
|
v-if="blacklistSelection.channelIdentityId"
|
||||||
@@ -505,18 +529,26 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div
|
||||||
|
v-if="blacklistSelection.sourceConversationType !== 'private'"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
<Label>{{ $t('bots.access.conversationId') }}</Label>
|
<Label>{{ $t('bots.access.conversationId') }}</Label>
|
||||||
<Input
|
<Input
|
||||||
v-model="blacklistSelection.sourceConversationId"
|
v-model="blacklistSelection.sourceConversationId"
|
||||||
|
:disabled="isObservedConversationLocked(blacklistSelection)"
|
||||||
:placeholder="$t('bots.access.conversationIdPlaceholder')"
|
:placeholder="$t('bots.access.conversationIdPlaceholder')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div
|
||||||
|
v-if="blacklistSelection.sourceConversationType !== 'private'"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
<Label>{{ $t('bots.access.threadId') }}</Label>
|
<Label>{{ $t('bots.access.threadId') }}</Label>
|
||||||
<Input
|
<Input
|
||||||
v-model="blacklistSelection.sourceThreadId"
|
v-model="blacklistSelection.sourceThreadId"
|
||||||
|
:disabled="isObservedConversationLocked(blacklistSelection)"
|
||||||
:placeholder="$t('bots.access.threadIdPlaceholder')"
|
:placeholder="$t('bots.access.threadIdPlaceholder')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -649,6 +681,7 @@ import ChannelBadge from '@/components/chat-list/channel-badge/index.vue'
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
botId: string
|
botId: string
|
||||||
|
botType?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -656,6 +689,8 @@ const queryCache = useQueryCache()
|
|||||||
const deletingRuleId = ref('')
|
const deletingRuleId = ref('')
|
||||||
const allowGuestDraft = ref(false)
|
const allowGuestDraft = ref(false)
|
||||||
|
|
||||||
|
const isPersonalBot = computed(() => props.botType === 'personal')
|
||||||
|
|
||||||
type RuleSelection = {
|
type RuleSelection = {
|
||||||
userId: string
|
userId: string
|
||||||
channelIdentityId: string
|
channelIdentityId: string
|
||||||
@@ -710,6 +745,16 @@ function bindSelectionWatchers(selection: RuleSelection) {
|
|||||||
selection.sourceThreadId = ''
|
selection.sourceThreadId = ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
watch(() => selection.sourceConversationType, (value) => {
|
||||||
|
if (value === 'private') {
|
||||||
|
selection.observedConversationRouteId = ''
|
||||||
|
selection.sourceConversationId = ''
|
||||||
|
selection.sourceThreadId = ''
|
||||||
|
}
|
||||||
|
if (value !== 'thread') {
|
||||||
|
selection.sourceThreadId = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
bindSelectionWatchers(whitelistSelection)
|
bindSelectionWatchers(whitelistSelection)
|
||||||
@@ -930,14 +975,23 @@ function buildSourceScope(selection: RuleSelection): AclSourceScope | undefined
|
|||||||
function normalizePayload(selection: RuleSelection): AclUpsertRuleRequest | null {
|
function normalizePayload(selection: RuleSelection): AclUpsertRuleRequest | null {
|
||||||
const user_id = selection.userId.trim()
|
const user_id = selection.userId.trim()
|
||||||
const channel_identity_id = selection.channelIdentityId.trim()
|
const channel_identity_id = selection.channelIdentityId.trim()
|
||||||
|
const sourceConversationType = selection.sourceConversationType.trim()
|
||||||
if ((user_id && channel_identity_id) || (!user_id && !channel_identity_id)) {
|
if ((user_id && channel_identity_id) || (!user_id && !channel_identity_id)) {
|
||||||
toast.error(t('bots.access.validation'))
|
toast.error(t('bots.access.validation'))
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
if (selection.sourceConversationId.trim() && !['group', 'thread'].includes(sourceConversationType)) {
|
||||||
|
toast.error(t('bots.access.validationConversationRequiresGroupOrThread'))
|
||||||
|
return null
|
||||||
|
}
|
||||||
if (selection.sourceThreadId.trim() && !selection.sourceConversationId.trim()) {
|
if (selection.sourceThreadId.trim() && !selection.sourceConversationId.trim()) {
|
||||||
toast.error(t('bots.access.validationThreadRequiresConversation'))
|
toast.error(t('bots.access.validationThreadRequiresConversation'))
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
if (selection.sourceThreadId.trim() && sourceConversationType !== 'thread') {
|
||||||
|
toast.error(t('bots.access.validationThreadRequiresThreadType'))
|
||||||
|
return null
|
||||||
|
}
|
||||||
if ((selection.sourceConversationId.trim() || selection.sourceThreadId.trim()) && !selection.sourceChannel.trim()) {
|
if ((selection.sourceConversationId.trim() || selection.sourceThreadId.trim()) && !selection.sourceChannel.trim()) {
|
||||||
toast.error(t('bots.access.validationConversationRequiresChannel'))
|
toast.error(t('bots.access.validationConversationRequiresChannel'))
|
||||||
return null
|
return null
|
||||||
@@ -1030,6 +1084,10 @@ function clearSourceScope(selection: RuleSelection) {
|
|||||||
selection.sourceThreadId = ''
|
selection.sourceThreadId = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isObservedConversationLocked(selection: RuleSelection): boolean {
|
||||||
|
return !!selection.observedConversationRouteId
|
||||||
|
}
|
||||||
|
|
||||||
function formatRuleLabel(item: AclRule): string {
|
function formatRuleLabel(item: AclRule): string {
|
||||||
if (item.subject_kind === 'user') {
|
if (item.subject_kind === 'user') {
|
||||||
return item.user_display_name || item.user_username || item.user_id || '-'
|
return item.user_display_name || item.user_username || item.user_id || '-'
|
||||||
|
|||||||
@@ -91,6 +91,7 @@
|
|||||||
/>
|
/>
|
||||||
{{ statusLabel }}
|
{{ statusLabel }}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<span v-if="bot?.type">{{ botTypeLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,15 +283,15 @@ const tabList = computed(() => {
|
|||||||
{ value: 'email', label: 'bots.tabs.email', component: BotEmail, params: { 'bot-id': bot_id } },
|
{ value: 'email', label: 'bots.tabs.email', component: BotEmail, params: { 'bot-id': bot_id } },
|
||||||
{ value: 'container', label: 'bots.tabs.container', component: BotContainer, params: {} },
|
{ value: 'container', label: 'bots.tabs.container', component: BotContainer, params: {} },
|
||||||
{ value: 'terminal', label: 'bots.tabs.terminal', component: BotTerminal, params: { 'bot-id': bot_id } },
|
{ value: 'terminal', label: 'bots.tabs.terminal', component: BotTerminal, params: { 'bot-id': bot_id } },
|
||||||
{ value: 'files', label: 'bots.tabs.files', component: BotFiles, params: { 'bot-id': bot_id } },
|
{ value: 'files', label: 'bots.tabs.files', component: BotFiles, params: { 'bot-id': bot_id, 'bot-type': bot.value?.type } },
|
||||||
{ value: 'mcp', label: 'bots.tabs.mcp', component: BotMcp, params: { 'bot-id': bot_id } },
|
{ value: 'mcp', label: 'bots.tabs.mcp', component: BotMcp, params: { 'bot-id': bot_id } },
|
||||||
{ value: 'subagents', label: 'bots.tabs.subagents', component: BotSubagents, params: { 'bot-id': bot_id } },
|
{ value: 'subagents', label: 'bots.tabs.subagents', component: BotSubagents, params: { 'bot-id': bot_id } },
|
||||||
{ value: 'heartbeat', label: 'bots.tabs.heartbeat', component: BotHeartbeat, params: { 'bot-id': bot_id } },
|
{ value: 'heartbeat', label: 'bots.tabs.heartbeat', component: BotHeartbeat, params: { 'bot-id': bot_id } },
|
||||||
{ value: 'schedule', label: 'bots.tabs.schedule', component: BotSchedule, params: { 'bot-id': bot_id } },
|
{ value: 'schedule', label: 'bots.tabs.schedule', component: BotSchedule, params: { 'bot-id': bot_id } },
|
||||||
{ value: 'history', label: 'bots.tabs.history', component: BotHistory, params: { 'bot-id': bot_id } },
|
{ value: 'history', label: 'bots.tabs.history', component: BotHistory, params: { 'bot-id': bot_id } },
|
||||||
{ value: 'skills', label: 'bots.tabs.skills', component: BotSkills, params: { 'bot-id': bot_id } },
|
{ value: 'skills', label: 'bots.tabs.skills', component: BotSkills, params: { 'bot-id': bot_id } },
|
||||||
{ value: 'access', label: 'bots.tabs.access', component: BotAccess, params: { 'bot-id': bot_id } },
|
{ value: 'access', label: 'bots.tabs.access', component: BotAccess, params: { 'bot-id': bot_id, 'bot-type': bot.value?.type } },
|
||||||
{ value: 'settings', label: 'bots.tabs.settings', component: BotSettings, params: { 'bot-id': bot_id } }
|
{ value: 'settings', label: 'bots.tabs.settings', component: BotSettings, params: { 'bot-id': bot_id, 'bot-type': bot.value?.type } }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -368,6 +369,12 @@ const {
|
|||||||
statusVariant,
|
statusVariant,
|
||||||
} = useBotStatusMeta(bot, t)
|
} = useBotStatusMeta(bot, t)
|
||||||
|
|
||||||
|
const botTypeLabel = computed(() => {
|
||||||
|
const type = bot.value?.type
|
||||||
|
if (type === 'personal' || type === 'public') return t('bots.types.' + type)
|
||||||
|
return type ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
const checks = ref<BotCheck[]>([])
|
const checks = ref<BotCheck[]>([])
|
||||||
const checksLoading = ref(false)
|
const checksLoading = ref(false)
|
||||||
|
|
||||||
|
|||||||
+39
-8
@@ -164,23 +164,54 @@ DELETE FROM bot_history_messages
|
|||||||
WHERE bot_id = sqlc.arg(bot_id);
|
WHERE bot_id = sqlc.arg(bot_id);
|
||||||
|
|
||||||
-- name: ListObservedConversationsByChannelIdentity :many
|
-- name: ListObservedConversationsByChannelIdentity :many
|
||||||
|
WITH observed_routes AS (
|
||||||
|
SELECT
|
||||||
|
(i.header->>'route_id')::uuid AS route_id,
|
||||||
|
MAX(i.created_at)::timestamptz AS last_observed_at
|
||||||
|
FROM bot_inbox i
|
||||||
|
WHERE i.bot_id = sqlc.arg(bot_id)
|
||||||
|
AND i.header->>'channel-identity-id' = sqlc.arg(channel_identity_id)::text
|
||||||
|
AND COALESCE(i.header->>'route_id', '') != ''
|
||||||
|
GROUP BY (i.header->>'route_id')::uuid
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
m.route_id,
|
||||||
|
MAX(m.created_at)::timestamptz AS last_observed_at
|
||||||
|
FROM bot_history_messages m
|
||||||
|
WHERE m.bot_id = sqlc.arg(bot_id)
|
||||||
|
AND m.sender_channel_identity_id = sqlc.arg(channel_identity_id)::uuid
|
||||||
|
AND m.route_id IS NOT NULL
|
||||||
|
GROUP BY m.route_id
|
||||||
|
),
|
||||||
|
ranked_routes AS (
|
||||||
|
SELECT
|
||||||
|
route_id,
|
||||||
|
MAX(last_observed_at)::timestamptz AS last_observed_at
|
||||||
|
FROM observed_routes
|
||||||
|
GROUP BY route_id
|
||||||
|
)
|
||||||
SELECT
|
SELECT
|
||||||
r.id AS route_id,
|
r.id AS route_id,
|
||||||
r.channel_type AS channel,
|
r.channel_type AS channel,
|
||||||
COALESCE(r.conversation_type, '') AS conversation_type,
|
CASE
|
||||||
|
WHEN LOWER(COALESCE(r.conversation_type, '')) IN ('thread', 'topic') THEN 'thread'
|
||||||
|
ELSE 'group'
|
||||||
|
END AS conversation_type,
|
||||||
r.external_conversation_id AS conversation_id,
|
r.external_conversation_id AS conversation_id,
|
||||||
COALESCE(r.external_thread_id, '') AS thread_id,
|
COALESCE(r.external_thread_id, '') AS thread_id,
|
||||||
COALESCE(r.metadata->>'conversation_name', '')::text AS conversation_name,
|
COALESCE(r.metadata->>'conversation_name', '')::text AS conversation_name,
|
||||||
MAX(m.created_at)::timestamptz AS last_observed_at
|
rr.last_observed_at
|
||||||
FROM bot_history_messages m
|
FROM ranked_routes rr
|
||||||
JOIN bot_channel_routes r ON r.id = m.route_id
|
JOIN bot_channel_routes r ON r.id = rr.route_id
|
||||||
WHERE m.bot_id = sqlc.arg(bot_id)
|
WHERE LOWER(COALESCE(r.conversation_type, '')) NOT IN ('', 'p2p', 'private', 'direct', 'dm')
|
||||||
AND m.sender_channel_identity_id = sqlc.arg(channel_identity_id)
|
|
||||||
GROUP BY
|
GROUP BY
|
||||||
r.id,
|
r.id,
|
||||||
r.channel_type,
|
r.channel_type,
|
||||||
r.conversation_type,
|
r.conversation_type,
|
||||||
r.external_conversation_id,
|
r.external_conversation_id,
|
||||||
r.external_thread_id,
|
r.external_thread_id,
|
||||||
r.metadata
|
r.metadata,
|
||||||
ORDER BY MAX(m.created_at) DESC;
|
rr.last_observed_at
|
||||||
|
ORDER BY rr.last_observed_at DESC;
|
||||||
|
|||||||
@@ -199,13 +199,9 @@ func (s *Service) ListObservedConversationsByChannelIdentity(ctx context.Context
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pgChannelIdentityID, err := db.ParseUUID(channelIdentityID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rows, err := s.queries.ListObservedConversationsByChannelIdentity(ctx, sqlc.ListObservedConversationsByChannelIdentityParams{
|
rows, err := s.queries.ListObservedConversationsByChannelIdentity(ctx, sqlc.ListObservedConversationsByChannelIdentityParams{
|
||||||
BotID: pgBotID,
|
BotID: pgBotID,
|
||||||
ChannelIdentityID: pgChannelIdentityID,
|
ChannelIdentityID: strings.TrimSpace(channelIdentityID),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -545,31 +545,62 @@ func (q *Queries) ListMessagesSince(ctx context.Context, arg ListMessagesSincePa
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listObservedConversationsByChannelIdentity = `-- name: ListObservedConversationsByChannelIdentity :many
|
const listObservedConversationsByChannelIdentity = `-- name: ListObservedConversationsByChannelIdentity :many
|
||||||
|
WITH observed_routes AS (
|
||||||
|
SELECT
|
||||||
|
(i.header->>'route_id')::uuid AS route_id,
|
||||||
|
MAX(i.created_at)::timestamptz AS last_observed_at
|
||||||
|
FROM bot_inbox i
|
||||||
|
WHERE i.bot_id = $1
|
||||||
|
AND i.header->>'channel-identity-id' = $2::text
|
||||||
|
AND COALESCE(i.header->>'route_id', '') != ''
|
||||||
|
GROUP BY (i.header->>'route_id')::uuid
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
m.route_id,
|
||||||
|
MAX(m.created_at)::timestamptz AS last_observed_at
|
||||||
|
FROM bot_history_messages m
|
||||||
|
WHERE m.bot_id = $1
|
||||||
|
AND m.sender_channel_identity_id = $2::uuid
|
||||||
|
AND m.route_id IS NOT NULL
|
||||||
|
GROUP BY m.route_id
|
||||||
|
),
|
||||||
|
ranked_routes AS (
|
||||||
|
SELECT
|
||||||
|
route_id,
|
||||||
|
MAX(last_observed_at)::timestamptz AS last_observed_at
|
||||||
|
FROM observed_routes
|
||||||
|
GROUP BY route_id
|
||||||
|
)
|
||||||
SELECT
|
SELECT
|
||||||
r.id AS route_id,
|
r.id AS route_id,
|
||||||
r.channel_type AS channel,
|
r.channel_type AS channel,
|
||||||
COALESCE(r.conversation_type, '') AS conversation_type,
|
CASE
|
||||||
|
WHEN LOWER(COALESCE(r.conversation_type, '')) IN ('thread', 'topic') THEN 'thread'
|
||||||
|
ELSE 'group'
|
||||||
|
END AS conversation_type,
|
||||||
r.external_conversation_id AS conversation_id,
|
r.external_conversation_id AS conversation_id,
|
||||||
COALESCE(r.external_thread_id, '') AS thread_id,
|
COALESCE(r.external_thread_id, '') AS thread_id,
|
||||||
COALESCE(r.metadata->>'conversation_name', '')::text AS conversation_name,
|
COALESCE(r.metadata->>'conversation_name', '')::text AS conversation_name,
|
||||||
MAX(m.created_at)::timestamptz AS last_observed_at
|
rr.last_observed_at
|
||||||
FROM bot_history_messages m
|
FROM ranked_routes rr
|
||||||
JOIN bot_channel_routes r ON r.id = m.route_id
|
JOIN bot_channel_routes r ON r.id = rr.route_id
|
||||||
WHERE m.bot_id = $1
|
WHERE LOWER(COALESCE(r.conversation_type, '')) NOT IN ('', 'p2p', 'private', 'direct', 'dm')
|
||||||
AND m.sender_channel_identity_id = $2
|
|
||||||
GROUP BY
|
GROUP BY
|
||||||
r.id,
|
r.id,
|
||||||
r.channel_type,
|
r.channel_type,
|
||||||
r.conversation_type,
|
r.conversation_type,
|
||||||
r.external_conversation_id,
|
r.external_conversation_id,
|
||||||
r.external_thread_id,
|
r.external_thread_id,
|
||||||
r.metadata
|
r.metadata,
|
||||||
ORDER BY MAX(m.created_at) DESC
|
rr.last_observed_at
|
||||||
|
ORDER BY rr.last_observed_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
type ListObservedConversationsByChannelIdentityParams struct {
|
type ListObservedConversationsByChannelIdentityParams struct {
|
||||||
BotID pgtype.UUID `json:"bot_id"`
|
BotID pgtype.UUID `json:"bot_id"`
|
||||||
ChannelIdentityID pgtype.UUID `json:"channel_identity_id"`
|
ChannelIdentityID string `json:"channel_identity_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListObservedConversationsByChannelIdentityRow struct {
|
type ListObservedConversationsByChannelIdentityRow struct {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { MCPConnection } from './mcp'
|
|||||||
|
|
||||||
export interface IdentityContext {
|
export interface IdentityContext {
|
||||||
botId: string
|
botId: string
|
||||||
containerId: string
|
containerId?: string
|
||||||
channelIdentityId: string
|
channelIdentityId?: string
|
||||||
displayName: string
|
displayName?: string
|
||||||
currentPlatform?: string
|
currentPlatform?: string
|
||||||
replyTarget?: string
|
replyTarget?: string
|
||||||
conversationType?: string
|
conversationType?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user