diff --git a/apps/web/package.json b/apps/web/package.json
index bf6cdfa6..3278c7ea 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -23,6 +23,7 @@
"@tanstack/vue-table": "^8.21.3",
"@vee-validate/zod": "^4.15.1",
"@vueuse/core": "^14.1.0",
+ "@vueuse/integrations": "^14.2.1",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-serialize": "^0.14.0",
"@xterm/xterm": "^6.0.0",
@@ -38,6 +39,7 @@
"pinia-plugin-persistedstate": "^4.7.1",
"qrcode": "^1.5.4",
"shiki": "^3.21.0",
+ "sortablejs": "^1.15.7",
"stream-markdown": "^0.0.13",
"stream-monaco": "^0.0.18",
"tailwindcss": "^4.2.2",
diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json
index 08737c77..166e05bf 100644
--- a/apps/web/src/i18n/locales/en.json
+++ b/apps/web/src/i18n/locales/en.json
@@ -34,7 +34,9 @@
"loadFailed": "Failed to load",
"saveFailed": "Failed to save",
"createdAt": "Created at",
- "none": "None"
+ "none": "None",
+ "yes": "Yes",
+ "no": "No"
},
"auth": {
"welcome": "Welcome Back",
@@ -821,8 +823,8 @@
"compactionModelPlaceholder": "Use chat model (default)",
"browserContext": "Browser Context",
"browserContextPlaceholder": "Select browser context (disabled if empty)",
- "allowGuest": "Allow Guest Access",
- "allowGuestPersonalHint": "Personal bots do not support guest access. Use a public bot instead.",
+ "allowGuest": "Default ACL Effect",
+ "allowGuestPersonalHint": "ACL rules apply to all bot types.",
"loopDetectionTitle": "Detect and auto-block output loops",
"searchModel": "Search models…",
"noModel": "No models available",
@@ -834,45 +836,47 @@
},
"access": {
"title": "Access Control",
- "subtitle": "Guest chat trigger access is controlled by allow guest, whitelist, and blacklist together.",
- "allowGuestDescription": "When enabled, guests may trigger chat by default. Blacklist rules still take precedence.",
- "saveGuestAccess": "Save Guest Access",
- "guestAccessSaved": "Guest access updated",
- "guestRulesTitle": "Rule Summary",
- "guestRulesDescription": "Owners and members always bypass these rules. The rules below only apply to guest chat triggers.",
- "whitelistTitle": "Whitelist",
- "whitelistDescription": "Whitelisted guests can still trigger chat even when guest access is not open.",
- "blacklistTitle": "Blacklist",
- "blacklistDescription": "Blacklisted guests are denied chat trigger even when guest access is open.",
- "userSelector": "Select User",
- "identitySelector": "Select Platform Identity",
- "selectUser": "Search and select a user",
+ "subtitle": "Priority-ordered rules control who can trigger this bot's chat. The first matching rule wins; when no rule matches, the default effect applies.",
+ "defaultEffectTitle": "Default Effect",
+ "defaultEffectDescription": "Applied when no rule matches an inbound message.",
+ "defaultEffectSaved": "Default effect updated",
+ "effectAllow": "Allow",
+ "effectDeny": "Deny",
+ "rulesTitle": "Rules",
+ "rulesDescription": "Drag by the handle to reorder. Lower numbers are evaluated first; the first matching rule's effect is applied.",
+ "addRule": "Add Rule",
+ "editRule": "Edit Rule",
+ "rulesEmpty": "No rules",
+ "rulesEmptyDescription": "Add a rule to control access beyond the default effect.",
+ "priority": "Priority",
+ "priorityPlaceholder": "e.g. 100",
+ "dragToReorder": "Drag to reorder",
+ "rulesReordered": "Rule order updated",
+ "reorderFailed": "Failed to update rule order",
+ "enabled": "Enabled",
+ "effect": "Effect",
+ "subjectKind": "Subject",
+ "subjectAll": "All",
+ "subjectChannelType": "Platform Type",
+ "subjectChannelIdentity": "Identity",
+ "subjectAllLabel": "Everyone",
+ "subjectChannelTypeLabel": "{channel} platform users",
+ "channelType": "Platform Type",
+ "channelTypePlaceholder": "Enter platform type (e.g. telegram)",
+ "identitySelector": "Platform Identity",
"selectIdentity": "Search and select a platform identity",
- "searchUser": "Search users",
"searchIdentity": "Search platform identities",
- "noUserCandidates": "No user candidates",
"noIdentityCandidates": "No platform identity candidates",
- "userId": "User ID",
- "userIdPlaceholder": "Enter user ID",
- "channelIdentityId": "Channel Identity ID",
- "channelIdentityIdPlaceholder": "Enter channel identity ID",
- "addWhitelist": "Add to Whitelist",
- "addBlacklist": "Add to Blacklist",
- "clearSelection": "Clear Selection",
- "whitelistEmpty": "No whitelist rules yet",
- "blacklistEmpty": "No blacklist rules yet",
- "validation": "Fill exactly one subject: user_id or channel_identity_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",
- "validationThreadRequiresThreadType": "Thread ID restrictions require conversation type to be Thread",
- "whitelistSaved": "Whitelist updated",
- "blacklistSaved": "Blacklist updated",
- "saveFailed": "Failed to save access rule",
+ "description": "Description",
+ "descriptionPlaceholder": "Optional note",
+ "deleteConfirmTitle": "Delete Rule",
+ "deleteConfirmDescription": "Are you sure you want to delete this rule?",
+ "ruleSaved": "Rule saved",
+ "saveFailed": "Failed to save",
"deleteSuccess": "Rule deleted",
"deleteFailed": "Failed to delete rule",
"sourceScopeTitle": "Source Scope",
- "sourceScopeDescription": "Optionally restrict a rule to a platform, conversation type, or specific conversation/thread.",
+ "sourceScopeDescription": "Optionally restrict this rule to a specific platform, conversation type, or conversation/thread.",
"sourceChannel": "Source Platform",
"anyChannel": "Any platform",
"conversationType": "Conversation Type",
@@ -880,12 +884,16 @@
"privateConversationType": "Private",
"groupConversationType": "Group",
"threadConversationType": "Thread",
- "observedConversation": "Observed Group Or Thread",
- "selectObservedConversation": "Select an observed group or thread",
- "searchObservedConversation": "Search observed groups or threads",
- "noObservedConversations": "No observed groups or threads",
- "selectIdentityFirst": "Select a platform identity first to load observed groups or threads",
- "observedConversationHint": "Use this only for group or thread rules. Private access should be controlled by conversation type alone.",
+ "conversationSource": "Conversation",
+ "conversationSourceDescription": "Search by name, platform, or conversation ID. Lists group/thread routes this bot has seen — either for the selected identity, or for the selected platform type.",
+ "selectConversationSource": "Search or select a conversation",
+ "searchConversationSource": "Search conversations",
+ "noObservedConversations": "No matching conversations in history yet. Use manual IDs below, or ensure this bot has messages in those chats (including private).",
+ "manualConversationIds": "Manual conversation IDs",
+ "manualConversationIdsHint": "If the conversation is not listed, enter the raw conversation ID and thread ID from the platform.",
+ "pickIdentityForConversationSearch": "Choose a platform identity above to search conversations observed for this bot.",
+ "pickChannelTypeForConversationSearch": "Enter or choose a platform type above to search conversations this bot has seen on that platform.",
+ "conversationIdManualHint": "Manual entry only. Search requires a platform identity.",
"conversationId": "Conversation ID",
"conversationIdPlaceholder": "Enter conversation ID",
"threadId": "Thread ID",
diff --git a/apps/web/src/i18n/locales/zh.json b/apps/web/src/i18n/locales/zh.json
index 723cda76..3fa8941b 100644
--- a/apps/web/src/i18n/locales/zh.json
+++ b/apps/web/src/i18n/locales/zh.json
@@ -34,7 +34,9 @@
"loadFailed": "加载失败",
"saveFailed": "保存失败",
"createdAt": "创建时间",
- "none": "无"
+ "none": "无",
+ "yes": "是",
+ "no": "否"
},
"auth": {
"welcome": "欢迎回来",
@@ -817,8 +819,8 @@
"compactionModelPlaceholder": "使用聊天模型(默认)",
"browserContext": "浏览器上下文",
"browserContextPlaceholder": "选择浏览器上下文(未配置时不启用)",
- "allowGuest": "允许游客访问",
- "allowGuestPersonalHint": "个人 Bot 不支持游客访问,请使用公开 Bot。",
+ "allowGuest": "ACL 默认行为",
+ "allowGuestPersonalHint": "ACL 规则适用于所有类型的 Bot。",
"loopDetectionTitle": "自动检测并阻止模型循环输出",
"searchModel": "搜索模型…",
"noModel": "暂无可选模型",
@@ -830,41 +832,43 @@
},
"access": {
"title": "访问控制",
- "subtitle": "游客聊天触发权限由 allow guest、白名单和黑名单共同决定。",
- "allowGuestDescription": "开启后,游客默认可触发聊天;黑名单仍然会优先阻止。",
- "saveGuestAccess": "保存游客访问设置",
- "guestAccessSaved": "游客访问设置已保存",
- "guestRulesTitle": "规则说明",
- "guestRulesDescription": "Owner 和成员始终可用;这里的规则只作用于游客的聊天触发。",
- "whitelistTitle": "白名单",
- "whitelistDescription": "当未开放游客时,白名单中的游客仍可触发聊天。",
- "blacklistTitle": "黑名单",
- "blacklistDescription": "即使已开放游客,黑名单中的游客也会被拒绝触发聊天。",
- "userSelector": "选择用户",
- "identitySelector": "选择平台身份",
- "selectUser": "搜索并选择用户",
+ "subtitle": "按优先级排序的规则控制谁可以触发此 Bot 的聊天。首条匹配规则生效;无规则匹配时采用默认行为。",
+ "defaultEffectTitle": "默认行为",
+ "defaultEffectDescription": "当没有规则匹配入站消息时应用此策略。",
+ "defaultEffectSaved": "默认行为已更新",
+ "effectAllow": "允许",
+ "effectDeny": "拒绝",
+ "rulesTitle": "规则",
+ "rulesDescription": "拖动左侧手柄调整顺序。数字越小越先评估,首条匹配规则的行为生效。",
+ "addRule": "添加规则",
+ "editRule": "编辑规则",
+ "rulesEmpty": "暂无规则",
+ "rulesEmptyDescription": "添加规则以在默认行为之外控制访问。",
+ "priority": "优先级",
+ "priorityPlaceholder": "例如 100",
+ "dragToReorder": "拖动排序",
+ "rulesReordered": "规则顺序已更新",
+ "reorderFailed": "更新规则顺序失败",
+ "enabled": "启用",
+ "effect": "行为",
+ "subjectKind": "主体",
+ "subjectAll": "所有人",
+ "subjectChannelType": "平台类型",
+ "subjectChannelIdentity": "身份",
+ "subjectAllLabel": "所有人",
+ "subjectChannelTypeLabel": "{channel} 平台用户",
+ "channelType": "平台类型",
+ "channelTypePlaceholder": "输入平台类型(如 telegram)",
+ "identitySelector": "平台身份",
"selectIdentity": "搜索并选择平台身份",
- "searchUser": "搜索用户",
"searchIdentity": "搜索平台身份",
- "noUserCandidates": "暂无可选用户",
"noIdentityCandidates": "暂无可选平台身份",
- "userId": "用户 ID",
- "userIdPlaceholder": "输入用户 ID",
- "channelIdentityId": "Channel Identity ID",
- "channelIdentityIdPlaceholder": "输入 channel identity ID",
- "addWhitelist": "添加到白名单",
- "addBlacklist": "添加到黑名单",
- "clearSelection": "清空选择",
- "whitelistEmpty": "暂无白名单规则",
- "blacklistEmpty": "暂无黑名单规则",
- "validation": "请只填写一个主体:user_id 或 channel_identity_id",
- "validationConversationRequiresChannel": "按会话或线程限制时,请先选择来源平台",
- "validationConversationRequiresGroupOrThread": "会话 ID 只支持用于群聊或线程规则",
- "validationThreadRequiresConversation": "填写线程 ID 前必须先填写会话 ID",
- "validationThreadRequiresThreadType": "线程 ID 只支持用于线程规则",
- "whitelistSaved": "白名单已更新",
- "blacklistSaved": "黑名单已更新",
- "saveFailed": "保存访问规则失败",
+ "description": "描述",
+ "descriptionPlaceholder": "可选备注",
+ "deleteConfirmTitle": "删除规则",
+ "deleteConfirmDescription": "确定要删除此规则吗?",
+ "ruleSaved": "规则已保存",
+ "saveFailed": "保存失败",
"deleteSuccess": "规则已删除",
"deleteFailed": "删除规则失败",
"sourceScopeTitle": "来源范围",
@@ -876,12 +880,16 @@
"privateConversationType": "私聊",
"groupConversationType": "群聊",
"threadConversationType": "线程",
- "observedConversation": "历史群聊或线程",
- "selectObservedConversation": "选择一个历史群聊或线程",
- "searchObservedConversation": "搜索历史群聊或线程",
- "noObservedConversations": "暂无历史群聊或线程",
- "selectIdentityFirst": "请先选择平台身份以加载历史群聊或线程",
- "observedConversationHint": "这里只用于群聊或线程规则。私聊访问只需要通过会话类型控制。",
+ "conversationSource": "会话",
+ "conversationSourceDescription": "按名称、平台或会话 ID 搜索。列表为本 Bot 曾出现过的群聊/线程路由:选「平台身份」时按该身份筛选;选「平台类型」时按该平台上的全部会话。",
+ "selectConversationSource": "搜索或选择会话",
+ "searchConversationSource": "搜索会话",
+ "noObservedConversations": "暂无符合的历史会话。可手动填写 ID,或确认该 Bot 在对应会话中已有消息(含私聊)。",
+ "manualConversationIds": "手动填写会话 ID",
+ "manualConversationIdsHint": "若列表中没有目标会话,请从平台复制原始会话 ID、线程 ID。",
+ "pickIdentityForConversationSearch": "请先在上方选择平台身份,才能按历史记录搜索会话。",
+ "pickChannelTypeForConversationSearch": "请先在上方填写或选择平台类型,才能按该平台上的历史会话搜索。",
+ "conversationIdManualHint": "当前仅能手动填写。搜索会话需先选择平台身份。",
"conversationId": "会话 ID",
"conversationIdPlaceholder": "输入会话 ID",
"threadId": "线程 ID",
diff --git a/apps/web/src/pages/bots/components/bot-access.vue b/apps/web/src/pages/bots/components/bot-access.vue
index c2882359..59c48481 100644
--- a/apps/web/src/pages/bots/components/bot-access.vue
+++ b/apps/web/src/pages/bots/components/bot-access.vue
@@ -9,761 +9,584 @@
+
+
+
+
+ {{ $t('bots.access.defaultEffectTitle') }}
+
+
+ {{ $t('bots.access.defaultEffectDescription') }}
+
+
+
+
+
+
+
+
+
+
-
-
-
- {{ $t('bots.settings.allowGuest') }}
-
+
+
+
+ {{ $t('bots.access.rulesTitle') }}
+
- {{ $t('bots.access.allowGuestDescription') }}
+ {{ $t('bots.access.rulesDescription') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ rule.priority }}
+
+
+
+
+ {{ rule.effect === 'allow' ? $t('bots.access.effectAllow') : $t('bots.access.effectDeny') }}
+
+
+
+
+
+ {{ describeSubject(rule) }}
+
+
+ {{ describeScope(rule.source_scope) }}
+
+
+ {{ rule.description }}
+
+
+
+
+
handleToggleEnabled(rule, !!v)"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
allowGuestDraft = !!val"
- />
-
-
-
-
-
-
-
- {{ $t('bots.access.guestRulesTitle') }}
-
-
- {{ $t('bots.access.guestRulesDescription') }}
-
-
-
-
-
-
- {{ $t('bots.access.whitelistTitle') }}
-
-
- {{ $t('bots.access.whitelistDescription') }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ initials(option.label) }}
-
-
-
-
- {{ option.label }}
-
-
- {{ option.description }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ initials(option.label) }}
-
-
-
-
- {{ option.label }}
-
-
- {{ option.description }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t('bots.access.sourceScopeTitle') }}
-
-
- {{ $t('bots.access.sourceScopeDescription') }}
-
-
-
-
-
-
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t('bots.access.selectIdentityFirst') }}
-
-
-
- {{ $t('common.loading') }}
-
-
- {{ $t('bots.access.observedConversationHint') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t('common.loading') }}
-
-
- {{ $t('bots.access.whitelistEmpty') }}
-
-
-
-
-
-
-
-
- {{ initials(formatRuleLabel(item)) }}
-
-
-
-
-
-
- {{ formatRuleLabel(item) }}
-
-
- {{ formatRuleMeta(item) }}
-
-
-
-
-
-
-
-
-
-
-
- {{ $t('bots.access.blacklistTitle') }}
-
-
- {{ $t('bots.access.blacklistDescription') }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ initials(option.label) }}
-
-
-
-
- {{ option.label }}
-
-
- {{ option.description }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ initials(option.label) }}
-
-
-
-
- {{ option.label }}
-
-
- {{ option.description }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t('bots.access.sourceScopeTitle') }}
-
-
- {{ $t('bots.access.sourceScopeDescription') }}
-
-
-
-
-
-
-
- {{ blacklistSelection.sourceChannel || $t('bots.access.anyChannel') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t('bots.access.selectIdentityFirst') }}
-
-
-
- {{ $t('common.loading') }}
-
-
- {{ $t('bots.access.observedConversationHint') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t('common.loading') }}
-
-
- {{ $t('bots.access.blacklistEmpty') }}
-
-
-
-
-
-
-
-
- {{ initials(formatRuleLabel(item)) }}
-
-
-
-
-
-
- {{ formatRuleLabel(item) }}
-
-
- {{ formatRuleMeta(item) }}
-
-
-
-
-
-
-
+ {{ $t('common.save') }}
+
+
+
+
+
diff --git a/apps/web/src/pages/bots/components/bot-terminal.vue b/apps/web/src/pages/bots/components/bot-terminal.vue
index 2540272a..13a9c42b 100644
--- a/apps/web/src/pages/bots/components/bot-terminal.vue
+++ b/apps/web/src/pages/bots/components/bot-terminal.vue
@@ -388,13 +388,17 @@ onBeforeUnmount(() => {
}"
/>
{{ tab.label }}
-
+