From 3db4230142daf157b50e9644148186f995ef83be Mon Sep 17 00:00:00 2001 From: matevip Date: Thu, 30 Apr 2026 06:59:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(channels):=20redesign=20list=20page=20?= =?UTF-8?q?=E2=80=94=20show=20only=20configured=20channels,=20add=20hero?= =?UTF-8?q?=20empty=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../channel/controller/ChannelController.java | 16 ++ .../vip/mate/channel/model/ChannelEntity.java | 10 + .../h2/V63__channel_identity_json.sql | 6 + .../mysql/V63__channel_identity_json.sql | 13 + .../channels/ChannelOnboardingWizard.vue | 16 +- .../components/channels/ChannelTypePicker.vue | 76 ++++-- mateclaw-ui/src/i18n/locales/en-US.ts | 36 +++ mateclaw-ui/src/i18n/locales/zh-CN.ts | 36 +++ mateclaw-ui/src/types/index.ts | 3 + mateclaw-ui/src/views/Channels.vue | 238 +++++++++++++----- 10 files changed, 359 insertions(+), 91 deletions(-) create mode 100644 mateclaw-server/src/main/resources/db/migration/h2/V63__channel_identity_json.sql create mode 100644 mateclaw-server/src/main/resources/db/migration/mysql/V63__channel_identity_json.sql diff --git a/mateclaw-server/src/main/java/vip/mate/channel/controller/ChannelController.java b/mateclaw-server/src/main/java/vip/mate/channel/controller/ChannelController.java index 8653ac68..22e13bd7 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/controller/ChannelController.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/controller/ChannelController.java @@ -184,11 +184,27 @@ public class ChannelController { }); body.put("name", c.getName()); body.put("enabled", Boolean.TRUE.equals(c.getEnabled())); + body.put("identity", parseIdentity(c.getIdentityJson())); return body; }) .toList()); } + /** + * Parse identity_json into a map for the list-page card. Returns an + * empty map for legacy rows that have not been re-verified yet, so the + * frontend can render the type-level description as a fallback. + */ + private Map parseIdentity(String identityJson) { + if (identityJson == null || identityJson.isBlank()) return Collections.emptyMap(); + try { + return objectMapper.readValue(identityJson, new TypeReference<>() {}); + } catch (Exception e) { + log.debug("identity_json parse failed (treating as empty): {}", e.getMessage()); + return Collections.emptyMap(); + } + } + @RequireWorkspaceRole("admin") @Operation(summary = "Pre-flight: validate draft channel config without persisting") @PostMapping("/preflight") diff --git a/mateclaw-server/src/main/java/vip/mate/channel/model/ChannelEntity.java b/mateclaw-server/src/main/java/vip/mate/channel/model/ChannelEntity.java index 57630a0f..e588c8be 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/model/ChannelEntity.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/model/ChannelEntity.java @@ -34,6 +34,16 @@ public class ChannelEntity { @TableField(value = "config_json", updateStrategy = FieldStrategy.ALWAYS) private String configJson; + /** + * Identity snapshot from the most recent successful credential probe + * (RFC-084 follow-up). JSON-encoded {@code accountName / accountId / team / + * region / ...} payload. Surfaced on the list page so cards read + * "Connected as @MyBot" instead of the type-level description. Optional — + * legacy rows have it null until the next successful connect. + */ + @TableField(value = "identity_json", updateStrategy = FieldStrategy.ALWAYS) + private String identityJson; + /** 是否启用 */ private Boolean enabled; diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V63__channel_identity_json.sql b/mateclaw-server/src/main/resources/db/migration/h2/V63__channel_identity_json.sql new file mode 100644 index 00000000..fc64cc4c --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/h2/V63__channel_identity_json.sql @@ -0,0 +1,6 @@ +-- RFC-084 follow-up: persist the identity returned by ChannelVerifier so +-- the channel list can show "Connected as @MyBot" instead of generic +-- type-level descriptions. Populated on wizard create from VerificationResult; +-- refreshed by adapters on first successful connect. + +ALTER TABLE mate_channel ADD COLUMN IF NOT EXISTS identity_json TEXT; diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V63__channel_identity_json.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V63__channel_identity_json.sql new file mode 100644 index 00000000..ab6f9351 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V63__channel_identity_json.sql @@ -0,0 +1,13 @@ +-- RFC-084 follow-up: persist the identity returned by ChannelVerifier so +-- the channel list can show "Connected as @MyBot" instead of generic +-- type-level descriptions. Populated on wizard create from VerificationResult; +-- refreshed by adapters on first successful connect. + +SET @col_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'mate_channel' + AND COLUMN_NAME = 'identity_json'); +SET @stmt := IF(@col_exists = 0, + 'ALTER TABLE mate_channel ADD COLUMN identity_json TEXT', + 'SELECT 1'); +PREPARE s FROM @stmt; EXECUTE s; DEALLOCATE PREPARE s; diff --git a/mateclaw-ui/src/components/channels/ChannelOnboardingWizard.vue b/mateclaw-ui/src/components/channels/ChannelOnboardingWizard.vue index 534925a3..b7697e35 100644 --- a/mateclaw-ui/src/components/channels/ChannelOnboardingWizard.vue +++ b/mateclaw-ui/src/components/channels/ChannelOnboardingWizard.vue @@ -631,7 +631,21 @@ async function onDone() { accessControl: defaultAccessControl(), renderConfig: defaultRenderConfig(), }) - const payload: Partial = { ...form.value, configJson, enabled: true } + // Persist identity from the verifyResult so the list page can show + // "Connected as @MyBot" without waiting for the next adapter probe. + // Skipped channels (web/webchat/webhook) carry an empty identity, so + // the field stays null in the DB and the legacy description path is + // still available as a fallback. + const identityMap = verifyResult.value?.identity || {} + const identityJson = Object.keys(identityMap).length > 0 + ? JSON.stringify(identityMap) + : undefined + const payload: Partial = { + ...form.value, + configJson, + identityJson, + enabled: true, + } const res: any = await channelApi.create(payload) ElMessage.success(t('channels.messages.saveSuccess')) emit('created', res.data as Channel) diff --git a/mateclaw-ui/src/components/channels/ChannelTypePicker.vue b/mateclaw-ui/src/components/channels/ChannelTypePicker.vue index 4c75227b..a02e0ca6 100644 --- a/mateclaw-ui/src/components/channels/ChannelTypePicker.vue +++ b/mateclaw-ui/src/components/channels/ChannelTypePicker.vue @@ -2,23 +2,38 @@ - - +
-
+
- -
-
-
-
- -
-
-

{{ channel.name }}

- {{ channel.channelType }} -
-
-
- {{ channel.enabled ? t('channels.status.active') : t('channels.status.inactive') }} -
-
- {{ getConnectionIcon(channel) }} {{ getConnectionLabel(channel) }} -
-
-
-

{{ channel.description }}

- + +
+
+ + + +
+

{{ t('channels.empty.title') }}

+

{{ t('channels.empty.desc') }}

+ +
+ + +
@@ -120,7 +168,7 @@