From 839e63acda882a3449cec11f5bccf8dd63e009c6 Mon Sep 17 00:00:00 2001 From: BBQ <35603386+HoneyBBQ@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:15:41 +0800 Subject: [PATCH] feat(access): add guest chat ACL (#235) --- apps/web/src/i18n/locales/en.json | 60 + apps/web/src/i18n/locales/zh.json | 60 + .../src/pages/bots/components/bot-access.vue | 1170 +++++++++++++++++ .../pages/bots/components/bot-settings.vue | 19 - apps/web/src/pages/bots/detail.vue | 2 + cmd/agent/main.go | 11 +- cmd/memoh/serve.go | 11 +- db/migrations/0001_init.down.sql | 1 + db/migrations/0001_init.up.sql | 65 +- .../0031_chat_acl_remove_bot_members.down.sql | 43 + .../0031_chat_acl_remove_bot_members.up.sql | 53 + .../0032_source_aware_acl_scope.down.sql | 52 + .../0032_source_aware_acl_scope.up.sql | 69 + db/queries/acl.sql | 136 ++ db/queries/bots.sql | 39 +- db/queries/channel_identities.sql | 26 + db/queries/conversations.sql | 80 +- db/queries/messages.sql | 22 + db/queries/preauth.sql | 18 - db/queries/settings.sql | 6 +- db/queries/users.sql | 13 + devenv/docker-compose.yml | 44 +- internal/accounts/service.go | 22 + internal/acl/service.go | 474 +++++++ internal/acl/service_test.go | 389 ++++++ internal/acl/types.go | 131 ++ internal/bots/service.go | 213 +-- internal/bots/service_test.go | 98 +- internal/bots/types.go | 26 - internal/channel/adapters/discord/discord.go | 4 +- internal/channel/adapters/feishu/inbound.go | 22 +- .../channel/adapters/feishu/sender_profile.go | 9 +- internal/channel/adapters/qq/qq.go | 2 +- internal/channel/adapters/qq/receive.go | 20 +- internal/channel/adapters/qq/receive_test.go | 12 +- .../channel/adapters/telegram/telegram.go | 18 +- internal/channel/adapters/wecom/inbound.go | 4 +- internal/channel/adapters/wecom/wecom.go | 2 +- internal/channel/identities/service.go | 38 + internal/channel/identities/types.go | 7 + internal/channel/inbound/channel.go | 52 +- internal/channel/inbound/channel_test.go | 197 ++- internal/channel/inbound/identity.go | 116 +- internal/channel/inbound/identity_test.go | 248 ++-- internal/channel/inbound_test.go | 4 +- internal/channel/manager_integration_test.go | 2 +- internal/channel/route/service.go | 29 +- internal/channel/types.go | 29 +- internal/command/handler.go | 9 +- internal/command/handler_test.go | 21 +- internal/conversation/service.go | 134 +- internal/db/sqlc/acl.sql.go | 415 ++++++ internal/db/sqlc/bots.sql.go | 194 +-- internal/db/sqlc/channel_identities.sql.go | 80 ++ internal/db/sqlc/conversations.sql.go | 131 +- internal/db/sqlc/messages.sql.go | 66 + internal/db/sqlc/models.go | 35 +- internal/db/sqlc/preauth.sql.go | 91 -- internal/db/sqlc/settings.sql.go | 36 +- internal/db/sqlc/users.sql.go | 53 + internal/handlers/acl.go | 322 +++++ internal/handlers/containerd.go | 6 +- internal/handlers/heartbeat.go | 2 +- internal/handlers/inbox.go | 2 +- internal/handlers/local_channel.go | 6 +- internal/handlers/mcp.go | 2 +- internal/handlers/mcp_oauth.go | 2 +- internal/handlers/memory.go | 2 +- internal/handlers/message.go | 4 +- internal/handlers/preauth.go | 72 - internal/handlers/schedule.go | 2 +- internal/handlers/settings.go | 2 +- internal/handlers/subagent.go | 2 +- internal/handlers/token_usage.go | 2 +- internal/handlers/users.go | 108 +- internal/policy/service.go | 41 +- internal/preauth/service.go | 107 -- internal/preauth/types.go | 14 - internal/settings/service.go | 55 +- packages/sdk/src/@pinia/colada.gen.ts | 214 ++- packages/sdk/src/index.ts | 4 +- packages/sdk/src/sdk.gen.ts | 107 +- packages/sdk/src/types.gen.ts | 605 ++++++--- spec/docs.go | 873 +++++++++--- spec/swagger.json | 873 +++++++++--- spec/swagger.yaml | 578 ++++++-- 86 files changed, 6886 insertions(+), 2554 deletions(-) create mode 100644 apps/web/src/pages/bots/components/bot-access.vue create mode 100644 db/migrations/0031_chat_acl_remove_bot_members.down.sql create mode 100644 db/migrations/0031_chat_acl_remove_bot_members.up.sql create mode 100644 db/migrations/0032_source_aware_acl_scope.down.sql create mode 100644 db/migrations/0032_source_aware_acl_scope.up.sql create mode 100644 db/queries/acl.sql delete mode 100644 db/queries/preauth.sql create mode 100644 internal/acl/service.go create mode 100644 internal/acl/service_test.go create mode 100644 internal/acl/types.go create mode 100644 internal/db/sqlc/acl.sql.go delete mode 100644 internal/db/sqlc/preauth.sql.go create mode 100644 internal/handlers/acl.go delete mode 100644 internal/handlers/preauth.go delete mode 100644 internal/preauth/service.go delete mode 100644 internal/preauth/types.go diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json index 459ff9b5..326feabe 100644 --- a/apps/web/src/i18n/locales/en.json +++ b/apps/web/src/i18n/locales/en.json @@ -559,6 +559,7 @@ "skills": "Skills", "email": "Email", "files": "Files", + "access": "Access", "terminal": "Terminal", "settings": "Settings" }, @@ -767,6 +768,65 @@ "deleteBotDescription": "Deleting this bot cannot be undone. Proceed with caution.", "deleteBot": "Delete Bot" }, + "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", + "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", + "validationThreadRequiresConversation": "Thread ID requires a conversation ID", + "whitelistSaved": "Whitelist updated", + "blacklistSaved": "Blacklist updated", + "saveFailed": "Failed to save access rule", + "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.", + "sourceChannel": "Source Platform", + "anyChannel": "Any platform", + "conversationType": "Conversation Type", + "anyConversationType": "Any conversation type", + "privateConversationType": "Private", + "groupConversationType": "Group", + "threadConversationType": "Thread", + "observedConversation": "Observed Conversation", + "selectObservedConversation": "Select an observed conversation", + "searchObservedConversation": "Search observed conversations", + "noObservedConversations": "No observed conversations", + "selectIdentityFirst": "Select a platform identity first to load observed conversations", + "observedConversationHint": "Choosing an observed conversation auto-fills the platform, conversation ID, and thread ID.", + "conversationId": "Conversation ID", + "conversationIdPlaceholder": "Enter conversation ID", + "threadId": "Thread ID", + "threadIdPlaceholder": "Enter thread ID", + "clearScope": "Clear Scope", + "lastObserved": "Last seen" + }, "channels": { "title": "Platforms", "addChannel": "Add Platform", diff --git a/apps/web/src/i18n/locales/zh.json b/apps/web/src/i18n/locales/zh.json index ac848fa2..165e3b61 100644 --- a/apps/web/src/i18n/locales/zh.json +++ b/apps/web/src/i18n/locales/zh.json @@ -555,6 +555,7 @@ "skills": "技能", "email": "邮件", "files": "文件", + "access": "访问控制", "terminal": "终端", "settings": "设置" }, @@ -763,6 +764,65 @@ "deleteBotDescription": "删除此 Bot 后无法恢复,请谨慎操作。", "deleteBot": "删除 Bot" }, + "access": { + "title": "访问控制", + "subtitle": "游客聊天触发权限由 allow guest、白名单和黑名单共同决定。", + "allowGuestDescription": "开启后,游客默认可触发聊天;黑名单仍然会优先阻止。", + "saveGuestAccess": "保存游客访问设置", + "guestAccessSaved": "游客访问设置已保存", + "guestRulesTitle": "规则说明", + "guestRulesDescription": "Owner 和成员始终可用;这里的规则只作用于游客的聊天触发。", + "whitelistTitle": "白名单", + "whitelistDescription": "当未开放游客时,白名单中的游客仍可触发聊天。", + "blacklistTitle": "黑名单", + "blacklistDescription": "即使已开放游客,黑名单中的游客也会被拒绝触发聊天。", + "userSelector": "选择用户", + "identitySelector": "选择平台身份", + "selectUser": "搜索并选择用户", + "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": "按会话或线程限制时,请先选择来源平台", + "validationThreadRequiresConversation": "填写线程 ID 前必须先填写会话 ID", + "whitelistSaved": "白名单已更新", + "blacklistSaved": "黑名单已更新", + "saveFailed": "保存访问规则失败", + "deleteSuccess": "规则已删除", + "deleteFailed": "删除规则失败", + "sourceScopeTitle": "来源范围", + "sourceScopeDescription": "可选地将规则限制到某个平台、会话类型或指定会话/线程。", + "sourceChannel": "来源平台", + "anyChannel": "任意平台", + "conversationType": "会话类型", + "anyConversationType": "任意会话类型", + "privateConversationType": "私聊", + "groupConversationType": "群聊", + "threadConversationType": "线程", + "observedConversation": "历史会话", + "selectObservedConversation": "选择一个历史会话", + "searchObservedConversation": "搜索历史会话", + "noObservedConversations": "暂无历史会话", + "selectIdentityFirst": "请先选择平台身份以加载历史会话", + "observedConversationHint": "选择历史会话后会自动填充平台、会话 ID 和线程 ID。", + "conversationId": "会话 ID", + "conversationIdPlaceholder": "输入会话 ID", + "threadId": "线程 ID", + "threadIdPlaceholder": "输入线程 ID", + "clearScope": "清空范围", + "lastObserved": "最近出现" + }, "channels": { "title": "平台", "addChannel": "添加平台", diff --git a/apps/web/src/pages/bots/components/bot-access.vue b/apps/web/src/pages/bots/components/bot-access.vue new file mode 100644 index 00000000..e7031676 --- /dev/null +++ b/apps/web/src/pages/bots/components/bot-access.vue @@ -0,0 +1,1170 @@ + + + diff --git a/apps/web/src/pages/bots/components/bot-settings.vue b/apps/web/src/pages/bots/components/bot-settings.vue index 071d8053..c22fec3e 100644 --- a/apps/web/src/pages/bots/components/bot-settings.vue +++ b/apps/web/src/pages/bots/components/bot-settings.vue @@ -258,18 +258,6 @@ - - -