diff --git a/CHANGELOG.md b/CHANGELOG.md
index 229774de30..a6e9a76d13 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@ WeChat original voice can be prepared for playback, native quotes and retained l
### Added
+- One Bot can bind several apps of the same platform, such as two Lark apps: each app's DMs and @mentions reach the same Inbox and replies go out through the app that received them. **Bind app** lists every connected app and greys out the ones a Bot already uses, naming that Bot, and External identities lists the Bot's apps grouped by platform. A shared reply from a Bot with two apps in the same group refuses with a clear message ([#1110](https://github.com/BotHarness/BotHarness/issues/1110), [External identities guide](docs/channel-sidebar/external-identities.md)).
+
- An app's conversation list in **Edit identity** is now manageable: conversations sort into **Waiting**, **Active**, **Muted** and **Blocked**. **Mute** keeps messages in the Inbox without waking the Bot, **Rules** edits one group's collection and wake, and **Block** (confirmed) cancels unsent replies and keeps the conversation out even after restart or unbinding and rebinding; **Allow again** starts fresh with no backfill. A new **New conversations** setting chooses **Admit automatically** or **Ask me first**; waiting conversations keep only names and counts, never message text. At most 20 new conversations per app join automatically each hour and 500 stay active; beyond that they wait with the reason shown ([#1109](https://github.com/BotHarness/BotHarness/issues/1109), [External identities guide](docs/channel-sidebar/external-identities.md)).
- Slack, Discord and personal WeChat bind in one step too: after **Bind app**, DMs to a Slack or Discord app and @mentions of it in channels go straight to the Bot's Inbox with replies in the same thread; personal WeChat admits only DMs from the person who scanned it, never other contacts or groups. The bind dialog shows the real receiving state on every platform, and recorded group conversations follow the platform's global group intake and wake defaults ([#1112](https://github.com/BotHarness/BotHarness/issues/1112), [External identities guide](docs/channel-sidebar/external-identities.md)).
diff --git a/CHANGELOG.zh.md b/CHANGELOG.zh.md
index 433a8d34fa..e2c03f34e6 100644
--- a/CHANGELOG.zh.md
+++ b/CHANGELOG.zh.md
@@ -9,6 +9,8 @@
### Added
+- 一个 Bot 可以绑定同一平台的多个应用,例如两个飞书应用:每个应用的私聊和 @ 都进入同一个收件箱,并通过收到消息的应用回复。**绑定应用** 列出所有已连接的应用,已被 Bot 使用的应用置灰并注明是哪个 Bot;外部身份按平台列出这个 Bot 的应用。Bot 在同一个群里有两个应用时,转发来源的回复会明确拒绝([#1110](https://github.com/BotHarness/BotHarness/issues/1110),[外部身份教程](docs/channel-sidebar/external-identities.md))。
+
- **编辑外部身份** 里的会话列表现在可以管理:会话分为 **等待处理**、**活跃**、**已静音**、**已屏蔽**。**静音** 后消息照常进入收件箱但不唤醒 Bot,**规则** 单独调整一个群的收件和唤醒,**屏蔽**(需确认)会取消未发出的回复,重启、解绑再绑定后依然有效;**再次允许** 从下一条消息重新开始,不补收。新增 **新会话** 设置,可选 **自动接收** 或 **先问我**;等待中的会话只记录名称和次数,不保存消息内容。每个应用每小时最多自动加入 20 个新会话、最多 500 个活跃会话,超出时进入等待并显示原因([#1109](https://github.com/BotHarness/BotHarness/issues/1109),[外部身份教程](docs/channel-sidebar/external-identities.md))。
- Slack、Discord 和个人微信也能一步绑定:**绑定应用** 后,私聊 Slack 或 Discord 应用、或在频道里 @ 它,消息直接进入 Bot 收件箱并在原话题回复;个人微信只接收扫码者本人的私聊,其他联系人和群不会进入。绑定弹窗对所有平台显示真实接收状态,已记下的群会话跟随平台全局默认的群收件和唤醒设置([#1112](https://github.com/BotHarness/BotHarness/issues/1112),[外部身份教程](docs/channel-sidebar/external-identities.md))。
diff --git a/docs/assets/pr/1110-several-apps/README.md b/docs/assets/pr/1110-several-apps/README.md
new file mode 100644
index 0000000000..4ca9fc0cdf
--- /dev/null
+++ b/docs/assets/pr/1110-several-apps/README.md
@@ -0,0 +1,16 @@
+# #1110 Several apps per platform
+
+Captured from a real DSH Host (0.2.0-rc.1) with a simulated Lark provider that exposes four apps: Ada has two Lark apps, Bob has one, and one is unbound. The Bind app shots are opened from Bob.
+
+## Before
+
+A Bot could bind only one app per platform. Bind app greyed out a second Lark app, and nothing showed which Bot used which app.
+
+## After
+
+| | zh light | en dark |
+| ------------------------------------------- | ----------------------------- | ---------------------------- |
+| Two Lark apps on Ada |  |  |
+| Bind app lists every app with its owner Bot |  |  |
+
+The remaining light/dark and zh/en variants are in this folder.
diff --git a/docs/assets/pr/1110-several-apps/bind-all-en-dark.webp b/docs/assets/pr/1110-several-apps/bind-all-en-dark.webp
new file mode 100644
index 0000000000..7fd0194360
Binary files /dev/null and b/docs/assets/pr/1110-several-apps/bind-all-en-dark.webp differ
diff --git a/docs/assets/pr/1110-several-apps/bind-all-en-light.webp b/docs/assets/pr/1110-several-apps/bind-all-en-light.webp
new file mode 100644
index 0000000000..95ff86a396
Binary files /dev/null and b/docs/assets/pr/1110-several-apps/bind-all-en-light.webp differ
diff --git a/docs/assets/pr/1110-several-apps/bind-all-zh-dark.webp b/docs/assets/pr/1110-several-apps/bind-all-zh-dark.webp
new file mode 100644
index 0000000000..ce84cd5cd6
Binary files /dev/null and b/docs/assets/pr/1110-several-apps/bind-all-zh-dark.webp differ
diff --git a/docs/assets/pr/1110-several-apps/bind-all-zh-light.webp b/docs/assets/pr/1110-several-apps/bind-all-zh-light.webp
new file mode 100644
index 0000000000..dfbde8d97e
Binary files /dev/null and b/docs/assets/pr/1110-several-apps/bind-all-zh-light.webp differ
diff --git a/docs/assets/pr/1110-several-apps/identities-en-dark.webp b/docs/assets/pr/1110-several-apps/identities-en-dark.webp
new file mode 100644
index 0000000000..319cdaa9a3
Binary files /dev/null and b/docs/assets/pr/1110-several-apps/identities-en-dark.webp differ
diff --git a/docs/assets/pr/1110-several-apps/identities-en-light.webp b/docs/assets/pr/1110-several-apps/identities-en-light.webp
new file mode 100644
index 0000000000..0172bf24ce
Binary files /dev/null and b/docs/assets/pr/1110-several-apps/identities-en-light.webp differ
diff --git a/docs/assets/pr/1110-several-apps/identities-zh-dark.webp b/docs/assets/pr/1110-several-apps/identities-zh-dark.webp
new file mode 100644
index 0000000000..32d283cb5e
Binary files /dev/null and b/docs/assets/pr/1110-several-apps/identities-zh-dark.webp differ
diff --git a/docs/assets/pr/1110-several-apps/identities-zh-light.webp b/docs/assets/pr/1110-several-apps/identities-zh-light.webp
new file mode 100644
index 0000000000..48e4d0ab53
Binary files /dev/null and b/docs/assets/pr/1110-several-apps/identities-zh-light.webp differ
diff --git a/docs/channel-sidebar/external-identities.md b/docs/channel-sidebar/external-identities.md
index 7d05677710..f76ab27ee4 100644
--- a/docs/channel-sidebar/external-identities.md
+++ b/docs/channel-sidebar/external-identities.md
@@ -1,6 +1,6 @@
# External identities
-Open **Bot DM → Channel sidebar → External identities**. It lists the external apps bound to this Bot (Lark / Feishu, Slack, Discord, WeChat and so on). An app belongs to exactly one Bot; other Bots cannot borrow it.
+Open **Bot DM → Channel sidebar → External identities**. It lists the external apps bound to this Bot (Lark / Feishu, Slack, Discord, WeChat and so on). An app belongs to exactly one Bot; other Bots cannot borrow it. A Bot can bind several apps, including several of the same platform (for example two Lark apps): each app receives its own DMs and @mentions into the same Inbox, and the Bot replies through the app that received the message. Apps are listed grouped by platform.
**Binding an app is enough.** Once bound, DMs to the app and @mentions of it in groups it belongs to go straight to this Bot’s Inbox, and the Bot replies in the same conversation. You don’t need a saved delivery target, a conversation authorization or a reception switch. Who can reach the app is decided by each platform's settings, such as Lark's availability scope, group membership and app permissions, or Discord's channel permissions.
@@ -22,7 +22,7 @@ The same goes for Slack and Discord: DM the app or @mention it in a channel it b
1. First connect the app under **Settings → IM Bots**, following the guide for your platform.
2. Click **+ Bind app**.
-3. Choose the connected app under **App**. The dropdown is searchable: type to filter.
+3. Choose the connected app under **App**. The dropdown is searchable: type to filter. Every connected app is listed: apps a Bot already uses are greyed out and say which Bot (or **already bound to this Bot**), and disconnected apps say **not connected**. **Manage app credentials** opens **Settings → IM bots**.
4. Click **Bind app**. The dialog then shows the real receiving state:
- **Ready**: DMs and @mentions to the app now reach this Bot’s Inbox.
- **Connecting**: the app is still opening its receiver. Wait a moment.
diff --git a/docs/channel-sidebar/external-identities.zh.md b/docs/channel-sidebar/external-identities.zh.md
index 4f5b4b74ba..f6ade474d6 100644
--- a/docs/channel-sidebar/external-identities.zh.md
+++ b/docs/channel-sidebar/external-identities.zh.md
@@ -1,6 +1,6 @@
# 外部身份
-打开 **Bot 私聊 → Channel sidebar → 外部身份**。这里列出绑定给这个 Bot 的外部应用(Lark / 飞书、Slack、Discord、微信等)。一个应用只属于一个 Bot,其他 Bot 不能借用。
+打开 **Bot 私聊 → Channel sidebar → 外部身份**。这里列出绑定给这个 Bot 的外部应用(Lark / 飞书、Slack、Discord、微信等)。一个应用只属于一个 Bot,其他 Bot 不能借用。一个 Bot 可以绑定多个应用,同一平台也可以绑定多个(例如两个飞书应用):每个应用各自的私聊和 @ 都进入同一个收件箱,Bot 通过收到消息的那个应用回复。应用按平台分组列出。
**绑定应用就够了。** 绑定后,私聊这个应用、或在它所在的群里 @ 它,消息会直接进入这个 Bot 的收件箱,Bot 在原会话回复。不需要保存投递目标,也不需要授权会话或打开收件开关。谁能联系到这个应用,由各平台的设置决定,例如飞书的可用范围、群成员和应用权限,或 Discord 的频道权限。
@@ -22,7 +22,7 @@ Slack 和 Discord 同样适用:私聊应用、或在它所在的频道里 @
1. 先按对应平台的教程,在 **设置 → IM机器人** 连接应用。
2. 点击 **+ 绑定应用**。
-3. 在 **应用** 中选择已连接的应用。下拉框支持搜索,输入文字即可筛选。
+3. 在 **应用** 中选择已连接的应用。下拉框支持搜索,输入文字即可筛选。这里列出所有已连接的应用:已被 Bot 使用的应用置灰,并注明是哪个 Bot(或 **已绑定到这个 Bot**);未连接的应用注明 **未连接**。**管理应用凭据** 会打开 **设置 → IM机器人**。
4. 点击 **绑定应用**。弹窗随后显示真实的接收状态:
- **已就绪**:发给这个应用的私聊和群里 @ 它的消息,现在会进入这个 Bot 的收件箱。
- **正在连接**:应用正在建立接收连接,稍等片刻。
diff --git a/packages/client/src/client/external-entries.tsx b/packages/client/src/client/external-entries.tsx
index 4f21c70020..0714a61620 100644
--- a/packages/client/src/client/external-entries.tsx
+++ b/packages/client/src/client/external-entries.tsx
@@ -23,6 +23,7 @@ function IdentitiesForBot({
t: ChannelSidebarEntryProps['t'];
}): ReactElement {
const { snapshot, failed, refresh, mount } = useMessagingSnapshot(slug, actions);
+ const { bots } = useClientState();
const [panel, setPanel] = useState<'pairing' | 'approval'>();
const [busy, setBusy] = useState(false);
const [scopeFailed, setScopeFailed] = useState<'pairing' | 'approval'>();
@@ -68,6 +69,7 @@ function IdentitiesForBot({
await actions.messagingGroupPolicy(slug, grantId, input);
await refresh();
}}
+ botName={(owner) => bots.find((bot) => bot.slug === owner)?.displayName ?? owner}
/>
slug,
}: {
snapshot: MessagingSnapshot | undefined;
t: BotHarnessTranslate;
mutate(input: MessagingIdentityInput): Promise;
conversation(input: MessagingConversationInput): Promise;
rules(grantId: string, input: GroupReceptionInput): Promise;
+ botName?(slug: string): string;
}): ReactElement {
const [mode, setMode] = useState<'bind' | 'bound' | 'edit' | 'reconnect' | 'unbind'>();
const [selected, setSelected] = useState();
@@ -127,40 +130,46 @@ export function ExternalIdentityList({