From 609ca49cf516fad1fc7bec4012441ecced64024b Mon Sep 17 00:00:00 2001 From: AlexMa233 Date: Sun, 22 Mar 2026 21:55:34 +0800 Subject: [PATCH] feat: matrix support (part 1) (#242) * feat(channel): add Matrix adapter support * fix(channel): prevent reasoning leaks in Matrix replies * fix(channel): persist Matrix sync cursors * fix(channel): improve Matrix markdown rendering * fix(channel): support Matrix attachments and multimodal history * fix(channel): expand Matrix reply media context * fix(handlers): allow media downloads for chat-access bots * fix(channel): classify Matrix DMs as direct chats * fix(channel): auto-join Matrix room invites * fix(channel): resolve Matrix room aliases for outbound send * fix(web): use Matrix brand icon in channel badges Replace the generic Matrix hashtag badge with the official brand asset so channel badges feel recognizable and fit the circular mask cleanly. * fix(channel): add Matrix room whitelist controls Let Matrix bots decide whether to auto-join invites and restrict inbound activity to allowed rooms or aliases. Expose the new controls in the web settings UI with line-based whitelist input so access rules stay explicit. * fix(channel): stabilize Matrix multimodal follow-ups and settings * fix(flow): avoid gosec panic on byte decoding * fix: fix golangci-lint * fix(channel): remove Matrix built-in ACL * fix(channel): preserve Matrix image captions * fix(channel): validate Matrix homeserver and sync access Fail Matrix connections early when the homeserver, access token, or /sync capability is misconfigured so bot health checks surface actionable errors. * fix(channel): preserve optional toggles and relax Matrix startup validation * fix(channel): tighten Matrix mention fallback parsing * fix(flow): skip structured assistant tool-call outputs * fix(flow): resolve merged resolver duplication Keep the internal agent resolver implementation after merging main so split helper files do not redeclare flow symbols. Restore user message normalization in sanitize and persistence paths to keep flow tests and command packages building. * fix(flow): remove unused merged resolver helper Drop the leftover truncate helper and import from the resolver merge fix so golangci-lint passes again without affecting flow behavior. --------- Co-authored-by: Acbox Liu --- apps/web/public/channels/matrix.svg | 5 + apps/web/src/i18n/locales/en.json | 6 + apps/web/src/i18n/locales/zh.json | 6 + .../pages/bots/components/bot-channels.vue | 17 +- .../components/channel-settings-panel.vue | 69 +- apps/web/src/pages/settings/index.vue | 2 +- apps/web/src/utils/channel-icons.ts | 8 +- cmd/agent/main.go | 9 + cmd/memoh/serve.go | 9 + db/queries/channels.sql | 9 +- go.mod | 1 + internal/channel/adapters/matrix/config.go | 213 ++ .../channel/adapters/matrix/config_test.go | 61 + internal/channel/adapters/matrix/markdown.go | 147 ++ .../channel/adapters/matrix/markdown_test.go | 40 + internal/channel/adapters/matrix/matrix.go | 1886 +++++++++++++++++ .../channel/adapters/matrix/matrix_test.go | 1018 +++++++++ internal/channel/adapters/matrix/stream.go | 231 ++ .../channel/adapters/matrix/stream_test.go | 189 ++ internal/channel/route/service_test.go | 13 + internal/channel/service.go | 22 + internal/channel/types.go | 2 +- internal/channel/types_test.go | 10 + .../conversation/flow/assistant_output.go | 57 +- .../flow/assistant_output_test.go | 98 + internal/conversation/flow/resolver.go | 193 +- internal/conversation/flow/resolver_store.go | 1 + internal/conversation/flow/resolver_test.go | 58 + internal/conversation/flow/resolver_util.go | 1 + internal/db/sqlc/channels.sql.go | 22 + internal/handlers/filemanager.go | 20 +- internal/handlers/filemanager_test.go | 22 + 32 files changed, 4394 insertions(+), 51 deletions(-) create mode 100644 apps/web/public/channels/matrix.svg create mode 100644 internal/channel/adapters/matrix/config.go create mode 100644 internal/channel/adapters/matrix/config_test.go create mode 100644 internal/channel/adapters/matrix/markdown.go create mode 100644 internal/channel/adapters/matrix/markdown_test.go create mode 100644 internal/channel/adapters/matrix/matrix.go create mode 100644 internal/channel/adapters/matrix/matrix_test.go create mode 100644 internal/channel/adapters/matrix/stream.go create mode 100644 internal/channel/adapters/matrix/stream_test.go create mode 100644 internal/channel/route/service_test.go create mode 100644 internal/channel/types_test.go create mode 100644 internal/conversation/flow/assistant_output_test.go create mode 100644 internal/handlers/filemanager_test.go diff --git a/apps/web/public/channels/matrix.svg b/apps/web/public/channels/matrix.svg new file mode 100644 index 00000000..979fc713 --- /dev/null +++ b/apps/web/public/channels/matrix.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json index 26cc6e28..71fd7f96 100644 --- a/apps/web/src/i18n/locales/en.json +++ b/apps/web/src/i18n/locales/en.json @@ -887,11 +887,16 @@ "webhookCallback": "WebHook Callback URL", "webhookCallbackHint": "Use this URL as the event subscription request URL in Feishu/Lark.", "webhookCallbackPending": "Save this platform configuration to generate the callback URL.", + "showSecretField": "Show {field}", + "hideSecretField": "Hide {field}", + "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", "types": { "feishu": "Feishu", "discord": "Discord", "qq": "QQ", + "matrix": "Matrix", "telegram": "Telegram", "web": "Web", "local": "Local" @@ -900,6 +905,7 @@ "feishu": "FS", "discord": "DC", "qq": "QQ", + "matrix": "MX", "telegram": "TG", "web": "Web", "local": "CLI" diff --git a/apps/web/src/i18n/locales/zh.json b/apps/web/src/i18n/locales/zh.json index 95584c98..399efb8f 100644 --- a/apps/web/src/i18n/locales/zh.json +++ b/apps/web/src/i18n/locales/zh.json @@ -883,11 +883,16 @@ "webhookCallback": "WebHook 回调地址", "webhookCallbackHint": "将该地址配置到飞书/Lark 事件订阅的请求 URL。", "webhookCallbackPending": "保存平台配置后会生成回调地址。", + "showSecretField": "显示{field}", + "hideSecretField": "隐藏{field}", + "feishuWebhookSecurityHint": "出于安全考虑,Webhook 模式必须配置 Encrypt Key 或 Verification Token 之一;未受保护的回调地址不应直接暴露在公网上。", + "feishuWebhookSecretRequired": "出于安全考虑,请至少配置 Encrypt Key 或 Verification Token 之一。", "noAvailableTypes": "所有平台类型均已配置", "types": { "feishu": "飞书", "discord": "Discord", "qq": "QQ", + "matrix": "Matrix", "telegram": "Telegram", "web": "Web", "local": "本地" @@ -896,6 +901,7 @@ "feishu": "飞", "discord": "DC", "qq": "QQ", + "matrix": "MX", "telegram": "TG", "web": "Web", "local": "CLI" diff --git a/apps/web/src/pages/bots/components/bot-channels.vue b/apps/web/src/pages/bots/components/bot-channels.vue index 5bc6b190..200733f8 100644 --- a/apps/web/src/pages/bots/components/bot-channels.vue +++ b/apps/web/src/pages/bots/components/bot-channels.vue @@ -199,10 +199,19 @@ const selectedItem = computed(() => allChannels.value.find((c) => c.meta.type === selectedType.value) ?? null, ) -watch(configuredChannels, (list) => { - if (list.length > 0 && !selectedType.value) { - selectedType.value = list[0].meta.type +watch(allChannels, (list) => { + if (list.length === 0) { + selectedType.value = null + return } + + const current = selectedType.value + if (current && list.some((item) => item.meta.type === current)) { + return + } + + const configured = list.find((item) => item.configured) + selectedType.value = configured?.meta.type ?? list[0].meta.type }, { immediate: true }) function addChannel(type: string) { @@ -214,6 +223,7 @@ function channelIcon(type: string): string { const icons: Record = { qq: 'QQ', telegram: 'TG', + matrix: 'MX', feishu: '飞', } return icons[type] ?? type.slice(0, 2).toUpperCase() @@ -223,6 +233,7 @@ function channelBadgeClass(type: string): string { const classes: Record = { qq: 'bg-sky-100 text-sky-700 dark:bg-sky-900 dark:text-sky-300', telegram: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300', + matrix: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300', feishu: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-300', } return classes[type] ?? 'bg-secondary text-secondary-foreground' diff --git a/apps/web/src/pages/bots/components/channel-settings-panel.vue b/apps/web/src/pages/bots/components/channel-settings-panel.vue index dc75e20d..6526be6b 100644 --- a/apps/web/src/pages/bots/components/channel-settings-panel.vue +++ b/apps/web/src/pages/bots/components/channel-settings-panel.vue @@ -121,14 +121,15 @@ >