Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))。
Expand Down
16 changes: 16 additions & 0 deletions docs/assets/pr/1110-several-apps/README.md
Original file line number Diff line number Diff line change
@@ -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 | ![](identities-zh-light.webp) | ![](identities-en-dark.webp) |
| Bind app lists every app with its owner Bot | ![](bind-all-zh-light.webp) | ![](bind-all-en-dark.webp) |

The remaining light/dark and zh/en variants are in this folder.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions docs/channel-sidebar/external-identities.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions docs/channel-sidebar/external-identities.zh.md
Original file line number Diff line number Diff line change
@@ -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 的频道权限。

Expand All @@ -22,7 +22,7 @@ Slack 和 Discord 同样适用:私聊应用、或在它所在的频道里 @

1. 先按对应平台的教程,在 **设置 → IM机器人** 连接应用。
2. 点击 **+ 绑定应用**。
3. 在 **应用** 中选择已连接的应用。下拉框支持搜索,输入文字即可筛选。
3. 在 **应用** 中选择已连接的应用。下拉框支持搜索,输入文字即可筛选。这里列出所有已连接的应用:已被 Bot 使用的应用置灰,并注明是哪个 Bot(或 **已绑定到这个 Bot**);未连接的应用注明 **未连接**。**管理应用凭据** 会打开 **设置 → IM机器人**。
4. 点击 **绑定应用**。弹窗随后显示真实的接收状态:
- **已就绪**:发给这个应用的私聊和群里 @ 它的消息,现在会进入这个 Bot 的收件箱。
- **正在连接**:应用正在建立接收连接,稍等片刻。
Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/client/external-entries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'>();
Expand Down Expand Up @@ -68,6 +69,7 @@ function IdentitiesForBot({
await actions.messagingGroupPolicy(slug, grantId, input);
await refresh();
}}
botName={(owner) => bots.find((bot) => bot.slug === owner)?.displayName ?? owner}
/>
<LarkSetupGuide
snapshot={snapshot}
Expand Down
125 changes: 84 additions & 41 deletions packages/client/src/client/external-identity-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { BotHarnessTranslate } from './locale.js';
import { Modal } from './modal.js';
import { MessagingHelp } from './messaging-help.js';
import { SidebarCardList, SidebarCardRow } from './sidebar-card.js';
import { openImSettings } from './bot-settings-open.js';

const AVAILABILITY = {
available: 'identity.state.available',
Expand All @@ -28,12 +29,14 @@ export function ExternalIdentityList({
mutate,
conversation,
rules,
botName = (slug) => slug,
}: {
snapshot: MessagingSnapshot | undefined;
t: BotHarnessTranslate;
mutate(input: MessagingIdentityInput): Promise<void>;
conversation(input: MessagingConversationInput): Promise<void>;
rules(grantId: string, input: GroupReceptionInput): Promise<void>;
botName?(slug: string): string;
}): ReactElement {
const [mode, setMode] = useState<'bind' | 'bound' | 'edit' | 'reconnect' | 'unbind'>();
const [selected, setSelected] = useState<MessagingIdentityView>();
Expand Down Expand Up @@ -127,40 +130,46 @@ export function ExternalIdentityList({
</p>
) : null}
<SidebarCardList label={t('identity.title')}>
{identities.map((row) => (
<SidebarCardRow
key={row.id}
icon="id-card"
title={row.name}
meta={`${platform(row.platform)} · ${t('identity.conversationCount', {
count: conversations(row).length,
})}`}
chips={<Tag tone="neutral">{t(AVAILABILITY[row.availability])}</Tag>}
muted={!row.enabled}
hint={t('identity.editFor', { name: row.name })}
dialog
disabled={busy}
onClick={() => open('edit', row)}
trailing={
<Switch
label={t('identity.enableFor', { name: row.name })}
checked={row.enabled}
disabled={busy}
onChange={(enabled) =>
void operate(() =>
mutate({
kind: 'update',
id: row.id,
expectedRevision: row.revision,
name: row.name,
enabled,
}),
)
}
/>
}
/>
))}
{[...identities]
.sort(
(a, b) =>
platform(a.platform).localeCompare(platform(b.platform)) ||
a.createdAt.localeCompare(b.createdAt),
)
.map((row) => (
<SidebarCardRow
key={row.id}
icon="id-card"
title={row.name}
meta={`${platform(row.platform)} · ${t('identity.conversationCount', {
count: conversations(row).length,
})}`}
chips={<Tag tone="neutral">{t(AVAILABILITY[row.availability])}</Tag>}
muted={!row.enabled}
hint={t('identity.editFor', { name: row.name })}
dialog
disabled={busy}
onClick={() => open('edit', row)}
trailing={
<Switch
label={t('identity.enableFor', { name: row.name })}
checked={row.enabled}
disabled={busy}
onChange={(enabled) =>
void operate(() =>
mutate({
kind: 'update',
id: row.id,
expectedRevision: row.revision,
name: row.name,
enabled,
}),
)
}
/>
}
/>
))}
<SidebarCardRow
anchor="lark-bind"
icon="plus"
Expand Down Expand Up @@ -266,17 +275,51 @@ export function ExternalIdentityList({
disabled={busy}
value={accountKey}
onSelect={setAccountKey}
options={accounts.map((account) => ({
value: account.providerId + ':' + account.ref,
label: account.name,
hint: platform(account.platform),
disabled:
!account.connected || identities.some((i) => i.platform === account.platform),
}))}
options={[...accounts]
.sort(
(a, b) =>
Number(a.boundBotSlug !== undefined || !a.connected) -
Number(b.boundBotSlug !== undefined || !b.connected) ||
platform(a.platform).localeCompare(platform(b.platform)) ||
a.name.localeCompare(b.name),
)
.map((account) => ({
value: account.providerId + ':' + account.ref,
label: account.name,
hint:
account.boundBotSlug === undefined
? account.connected
? platform(account.platform)
: t('identity.appOffline', { platform: platform(account.platform) })
: identities.some(
(i) =>
i.providerId === account.providerId &&
i.accountRef === account.ref,
)
? t('identity.appThisBot', { platform: platform(account.platform) })
: t('identity.appUsedBy', {
platform: platform(account.platform),
name: botName(account.boundBotSlug),
}),
disabled: !account.connected || account.boundBotSlug !== undefined,
}))}
/>
</label>
{!accounts.length ? <p>{t('im.setup')}</p> : null}
<p className="bh-muted">{t('identity.bindHint')}</p>
<div>
<Button
size="sm"
variant="outline"
disabled={busy}
onClick={() => {
setMode(undefined);
openImSettings(document, () => undefined);
}}
>
{t('identity.manageApps')}
</Button>
</div>
</>
) : mode === 'bound' && selectedAccount ? (
<>
Expand Down
13 changes: 11 additions & 2 deletions packages/client/src/client/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,10 @@ export const zh = {
'identity.kind.dm': '私聊',
'identity.kind.group': '群聊',
'identity.lastMessage': '最近消息 {time}',
'identity.appUsedBy': '{platform} · 已被 {name} 使用',
'identity.appThisBot': '{platform} · 已绑定到这个 Bot',
'identity.appOffline': '{platform} · 未连接',
'identity.manageApps': '管理应用凭据',
'identity.newConversations': '新会话',
'identity.newConversations.auto': '自动接收',
'identity.newConversations.ask': '先问我',
Expand Down Expand Up @@ -860,7 +864,8 @@ export const zh = {
'identity.unbind': '解绑',
'identity.unbindFor': '解绑身份:{name}',
'identity.displayName': '本地显示名称',
'identity.providerHint': '选择一个已在 IM 设置中连接的应用。应用凭据仍由 IM 插件管理。',
'identity.providerHint':
'这里列出所有已在 IM 设置中连接的应用,以及各自绑定的 Bot。一个应用只属于一个 Bot。',
'identity.bindHint':
'绑定后,私聊这个应用和在群里 @ 它的消息会直接进入这个 Bot 的收件箱,Bot 在原会话回复,不需要保存目标或授权会话。个人微信只接收扫码者本人的私聊。',
'identity.reconnectHint':
Expand Down Expand Up @@ -2956,6 +2961,10 @@ My relationship with the Human, established lore and topics to avoid: [fill in].
'identity.kind.dm': 'DM',
'identity.kind.group': 'Group',
'identity.lastMessage': 'Last message {time}',
'identity.appUsedBy': '{platform} · used by {name}',
'identity.appThisBot': '{platform} · already bound to this Bot',
'identity.appOffline': '{platform} · not connected',
'identity.manageApps': 'Manage app credentials',
'identity.newConversations': 'New conversations',
'identity.newConversations.auto': 'Admit automatically',
'identity.newConversations.ask': 'Ask me first',
Expand Down Expand Up @@ -3003,7 +3012,7 @@ My relationship with the Human, established lore and topics to avoid: [fill in].
'identity.unbindFor': 'Unbind identity: {name}',
'identity.displayName': 'Local display name',
'identity.providerHint':
'Choose an app already connected in IM settings. The IM plugin keeps its credentials.',
'Every app connected in IM settings is listed here with the Bot it is bound to. An app belongs to one Bot only.',
'identity.bindHint':
'Once bound, DMs to the app and @mentions of it in groups go straight to this Bot’s Inbox, and the Bot replies in place. No saved targets or conversation authorization needed. Personal WeChat only receives DMs from the person who scanned it.',
'identity.reconnectHint':
Expand Down
Loading
Loading