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 }} +

+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + {{ editingRule ? $t('bots.access.editRule') : $t('bots.access.addRule') }} + + + +
+
+ +
+ + {{ ruleForm.enabled ? $t('common.yes') : $t('common.no') }} +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+ +
+ + +
+ + + + +
+ + +
+ + + {{ $t('bots.access.sourceScopeTitle') }} + +
+

+ {{ $t('bots.access.sourceScopeDescription') }} +

+ + +
+ +
+ +
+
+ + +
+ +

+ {{ $t('bots.access.conversationSourceDescription') }} +

+ + + + +
+ + + {{ $t('bots.access.manualConversationIds') }} + +

+ {{ $t('bots.access.manualConversationIdsHint') }} +

+
+
+ + +
+
+ + +
+
+
+
+ + + + + +
+
+ + +
+ + +
+

- {{ $t('bots.settings.allowGuestPersonalHint') }} + {{ formError }}

- - - -
- -
- -
-

- {{ $t('bots.access.guestRulesTitle') }} -

-

- {{ $t('bots.access.guestRulesDescription') }} -

-
- -
-
-

- {{ $t('bots.access.whitelistTitle') }} -

-

- {{ $t('bots.access.whitelistDescription') }} -

-
- -
-
- - - - - -
-
- - - - - -
-
- -
-
-

- {{ $t('bots.access.sourceScopeTitle') }} -

-

- {{ $t('bots.access.sourceScopeDescription') }} -

-
- -
-
- -
+
- +
- -
- - - - - - - -
- -
- - -
- {{ $t('bots.access.selectIdentityFirst') }} -
-

- - -

-
- -
- - -
- -
- - -
-
- -
- -
-
- -
- - -
- - - -
- {{ $t('common.loading') }} -
-
- {{ $t('bots.access.whitelistEmpty') }} -
-
-
-
-
- - - - {{ initials(formatRuleLabel(item)) }} - - - -
-
-
- {{ formatRuleLabel(item) }} -
-
- {{ formatRuleMeta(item) }} -
-
-
- -
-
-
- -
-
-

- {{ $t('bots.access.blacklistTitle') }} -

-

- {{ $t('bots.access.blacklistDescription') }} -

-
- -
-
- - - - - -
-
- - - - - -
-
- -
-
-

- {{ $t('bots.access.sourceScopeTitle') }} -

-

- {{ $t('bots.access.sourceScopeDescription') }} -

-
- -
-
- -
- {{ blacklistSelection.sourceChannel || $t('bots.access.anyChannel') }} -
- - - - -
- -
- - - - - - - -
- -
- - -
- {{ $t('bots.access.selectIdentityFirst') }} -
-

- - -

-
- -
- - -
- -
- - -
-
- -
- -
-
- -
- - -
- - - -
- {{ $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 }} - +