diff --git a/CHANGELOG.md b/CHANGELOG.md index a6e9a76d13..59815de6fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,8 @@ WeChat original voice can be prepared for playback, native quotes and retained l ### Changed +- The External connectors entry no longer offers **Add connector** or **Authorize a conversation** while syncing is redesigned around connecting any app's conversation; it lists existing syncs and keeps **Save a send target (advanced)** for apps that can't post to conversations directly. A conversation row shows where it is synced, and its actions (**Mute**, **Rules**, **Block**) sit on their own row with a tooltip each. The Lark setup guide shrinks to three steps (connect the app, bind it, send a test message), and the Lark, Slack and WeChat guides are rewritten around **Bind app** ([#1113](https://github.com/BotHarness/BotHarness/issues/1113), [Lark guide](docs/lark-connection.md), [External connectors guide](docs/channel-sidebar/external-connectors.md)). + - A Bot's model and wake policy moved from its Profile to two new entries in the DM's Channel sidebar, **Model** and **Wake policy**. Each shows its current value in the collapsed header, lists one row per setting, and opens a dialog to edit; both can be reordered or hidden like other entries. The Model dialog now lists a **Main model** and a **Task model**, each picked from a searchable list with its reasoning effort; models can be set without any preset, a saved preset can still fill the dialog, and **Save as preset** keeps the current settings for reuse. **Attention policy** is now called **Wake policy** ([#1084](https://github.com/BotHarness/BotHarness/issues/1084), [Model guide](docs/channel-sidebar/model.md), [Wake policy guide](docs/channel-sidebar/wake-policy.md)). - A Bot's IM settings moved from its Profile to two new entries in the DM's Channel sidebar. **External identities** lists each bound identity as a row with its availability and an on/off switch, plus **Bind identity**, the Lark setup guide, IM administrator pairing and Lark approval notifications. **External connectors** lists one row per connector with its state and an on/off switch, plus **Add connector** and conversation authorization. Clicking a row opens a dialog to edit it, with the same options as before. **Channel connectors** are now called **External connectors**. The SOUL.md and MEMORY.md standing limits became one row under **Memory files** that opens a dialog. The moved forms now use the shared dropdowns, inputs and checkboxes ([#1085](https://github.com/BotHarness/BotHarness/issues/1085), [External identities guide](docs/channel-sidebar/external-identities.md), [External connectors guide](docs/channel-sidebar/external-connectors.md)). diff --git a/CHANGELOG.zh.md b/CHANGELOG.zh.md index e2c03f34e6..6fec2a1121 100644 --- a/CHANGELOG.zh.md +++ b/CHANGELOG.zh.md @@ -41,6 +41,8 @@ ### Changed +- 「外部连接器」不再提供 **添加外部连接器** 和 **授权外部会话**(同步正在围绕“接入任意应用的会话”重新设计),只列出已有的同步,并保留 **保存发送目标(高级)** 给不能直接发往会话的应用。会话行会显示已同步到哪里,操作按钮(**静音**、**规则**、**屏蔽**)单独一行并各带说明提示。Lark 配置引导缩减为三步(连接应用、绑定、发消息验证),Lark、Slack、微信连接教程围绕 **绑定应用** 重写([#1113](https://github.com/BotHarness/BotHarness/issues/1113)、[Lark 教程](docs/lark-connection.md)、[外部连接器说明](docs/channel-sidebar/external-connectors.md))。 + - Bot 的模型和唤醒策略从 Profile 移到私聊 Channel sidebar 的两个新分区 **模型** 与 **唤醒策略**:收起时标题右侧显示当前值,每项设置一行,点击打开弹窗修改,并可像其他分区一样排序或隐藏。模型弹窗改为 **主模型** 与 **执行模型** 两项,可输入过滤选择模型并选思考强度;不建预设也能直接保存,已有预设仍可用来填入,**另存为预设** 可把当前设置留作复用。**提醒策略** 改名为 **唤醒策略**([#1084](https://github.com/BotHarness/BotHarness/issues/1084),[模型教程](docs/channel-sidebar/model.md),[唤醒策略教程](docs/channel-sidebar/wake-policy.md))。 - Bot 的 IM 设置从 Profile 移到私聊 Channel sidebar 的两个新分区。**外部身份** 每个已绑定身份一行,显示可用状态和启用开关,下面是 **绑定身份**、Lark 配置引导、IM 管理员配对和 Lark 审批通知。**外部连接器** 每个连接器一行,显示状态和启用开关,下面是 **添加外部连接器** 和外部会话授权。点击一行打开弹窗修改,选项与原来一致。**频道连接器** 改名为 **外部连接器**。SOUL.md 和 MEMORY.md 的常驻记忆上限变成 **记忆文件** 下的一行,点击打开弹窗。迁移后的表单统一使用共享的下拉框、输入框和复选框([#1085](https://github.com/BotHarness/BotHarness/issues/1085),[外部身份教程](docs/channel-sidebar/external-identities.md),[外部连接器教程](docs/channel-sidebar/external-connectors.md))。 diff --git a/CONTEXT.md b/CONTEXT.md index b467acd62b..8c4d42236e 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -435,9 +435,25 @@ The membership state of a Channel that belongs to no Channel section. Ungrouped _Avoid_: default folder, inbox, fixed bottom bucket **Bridge**: -A configured connection from an external source, such as an IM conversation or later a webhook, to an explicit Channel or PersonaBot Inbox target; it carries inbound delivery and exposes outbound capabilities without becoming the Actor. The UI calls it an External connector (外部连接器); the PersonaBot's own external identity is a separate thing. +A configured connection from an external source, such as an IM conversation or later a webhook, to an explicit Channel or PersonaBot Inbox target; it carries inbound delivery and exposes outbound capabilities without becoming the Actor. For an IM conversation it is listed under External connectors (外部连接器); the PersonaBot's own external identity is a separate thing. _Avoid_: integration, adapter, connector (bare) +**App**: +The UI name (应用) for one authenticated Provider account, such as a Lark app, a Slack app, a Discord bot or a paired WeChat Bot. An App is bound to at most one PersonaBot; a PersonaBot may bind several Apps, including several of one platform. Settings → IM apps lists every App and the Bot that uses it. +_Avoid_: IM account (in UI copy), connector, integration + +**Default traffic**: +The messages an enabled external identity Binding admits without any per-conversation consent: direct messages to the App and group messages the qualified Provider reports as mentioning it. Where the App can be reached is decided on the platform; ordinary group text, followed threads and Channel syncs stay explicit. +_Avoid_: all messages, authorized traffic, open intake + +**Conversation entry**: +The per-conversation anchor of one Binding's traffic, stored as a Messaging Grant. An _implicit_ entry is recorded on the first admitted default-traffic message or first proactive post; an _explicit_ entry comes from a saved send target. A _held_ entry waits for the Human (Ask me first, or a bound was reached) and keeps only metadata. A _blocked_ conversation has a durable block keyed by Bot, App fingerprint, kind and ID that refuses it until **Allow again**. Muting an entry keeps admission but never wakes the Bot. +_Avoid_: authorization (for implicit entries), delivery target, subscription + +**New-conversation mode**: +A Binding's choice for a conversation that has no entry yet: `auto` (Admit automatically, the default) records an implicit entry and admits the message; `ask` (Ask me first) holds it. Auto-creation is bounded at 20 new entries per hour and 500 active entries per Binding. +_Avoid_: whitelist, approval mode, auto-reply + **Bot Inbox**: The PersonaBot-level view of Source Events admitted for its attention, whether or not an event belongs to a Channel. It is not a second content store: reading is an explicit act, and ignoring is allowed. _Avoid_: queue, mailbox, backlog diff --git a/CONTEXT.zh.md b/CONTEXT.zh.md index b1ec9e4687..040cb75bc9 100644 --- a/CONTEXT.zh.md +++ b/CONTEXT.zh.md @@ -433,9 +433,25 @@ Channel 不属于任何 Channel section 时的 membership state。未分组 Chan _避免使用_:default folder、inbox、fixed bottom bucket **Bridge**: -从 external source(如 IM 会话,未来还有 webhook)到某个显式 Channel 或 PersonaBot Inbox target 的已配置连接;它承载 inbound delivery 并暴露 outbound capability,但不会成为 Actor。UI 上称为「外部连接器 / External connector」;PersonaBot 自己的外部身份是另一回事。 +从 external source(如 IM 会话,未来还有 webhook)到某个显式 Channel 或 PersonaBot Inbox target 的已配置连接;它承载 inbound delivery 并暴露 outbound capability,但不会成为 Actor。对于 IM 会话,它列在「外部连接器 / External connector」中;PersonaBot 自己的外部身份是另一回事。 _避免使用_:integration、adapter、裸用 connector +**App**: +UI 上叫「应用」:一个已认证 Provider account 的名称,例如 Lark 应用、Slack 应用、Discord bot 或已配对的微信 Bot。一个应用最多绑定到一个 PersonaBot;一个 PersonaBot 可以绑定多个应用,包括同一平台的多个应用。「设置 → IM 应用」列出每个应用及使用它的 Bot。 +_避免使用_:UI 文案里的「IM账号」、connector、integration + +**Default traffic**: +启用的外部身份 Binding 无需逐会话同意即可接收的消息:发给应用的私聊,以及合格 Provider 报告为 @ 该应用的群消息。谁能触达应用由平台决定;群里的普通文字、跟进的话题和 Channel 同步仍需显式开启。 +_避免使用_:全部消息、已授权流量、开放收件 + +**Conversation entry**: +一个 Binding 的单个会话流量所挂靠的锚点,存为 Messaging Grant。_隐式_ 条目在第一条被接收的默认流量消息或第一次主动发送时记录;_显式_ 条目来自保存的发送目标。_等待处理_ 的条目等 Human 决定(先问我,或达到上限),只保留元数据。_已屏蔽_ 的会话有一个按 Bot、应用指纹、会话类型和 ID 记录的持久屏蔽,直到 **再次允许** 前都会被拒绝。静音的条目照常接收,但不会唤醒 Bot。 +_避免使用_:授权(指隐式条目时)、投递目标、订阅 + +**New-conversation mode**: +Binding 对尚无条目的会话的处理方式:`auto`(自动接收,默认)记录隐式条目并接收消息;`ask`(先问我)把它放到等待处理。自动创建有上限:每个 Binding 每小时 20 个新条目、同时 500 个活跃条目。 +_避免使用_:白名单、审批模式、自动回复 + **Bot Inbox**: PersonaBot 层级的 view,包含被 admit 供其 attention 的 Source Event,无论 event 是否属于某个 Channel。它不是第二个 content store:读取是一项显式行为,也允许 ignore。 _避免使用_:queue、mailbox、backlog diff --git a/apps/docs/public/guides/channel-sidebar/20b-external-connectors-en.webp b/apps/docs/public/guides/channel-sidebar/20b-external-connectors-en.webp new file mode 100644 index 0000000000..5903ed7579 Binary files /dev/null and b/apps/docs/public/guides/channel-sidebar/20b-external-connectors-en.webp differ diff --git a/apps/docs/public/guides/channel-sidebar/20b-external-connectors-zh.webp b/apps/docs/public/guides/channel-sidebar/20b-external-connectors-zh.webp new file mode 100644 index 0000000000..feea96edf5 Binary files /dev/null and b/apps/docs/public/guides/channel-sidebar/20b-external-connectors-zh.webp differ diff --git a/apps/docs/public/guides/lark/28-setup-guide.en.webp b/apps/docs/public/guides/lark/28-setup-guide.en.webp new file mode 100644 index 0000000000..3f55520947 Binary files /dev/null and b/apps/docs/public/guides/lark/28-setup-guide.en.webp differ diff --git a/apps/docs/public/guides/lark/28-setup-guide.zh.webp b/apps/docs/public/guides/lark/28-setup-guide.zh.webp new file mode 100644 index 0000000000..7ba97e3a07 Binary files /dev/null and b/apps/docs/public/guides/lark/28-setup-guide.zh.webp differ diff --git a/docs/architecture/botharness-architecture.en.md b/docs/architecture/botharness-architecture.en.md index 2b4c68125e..6df4a25875 100644 --- a/docs/architecture/botharness-architecture.en.md +++ b/docs/architecture/botharness-architecture.en.md @@ -219,10 +219,14 @@ sequenceDiagram M->>DB: append attempt and outcome facts ``` -A Source Event is the sole authority for content; Channels and Inboxes hold relationships only. A Reply follows the trusted Reply Route so the Host selects the source provider. A proactive post is a Service Action and requires both Provider Capability and a Human Service Grant. A SQLite transaction covers local facts only. External effects use an Outbox Intent, a stable idempotency identity, and bounded reconciliation without claiming exactly-once delivery. An outcome that cannot be proven becomes `unknown-outcome` for Human resolution. +A Source Event is the sole authority for content; Channels and Inboxes hold relationships only. A Reply follows the trusted Reply Route so the Host selects the source provider. A proactive post is a Service Action and requires Provider Capability plus either the Bot's conversation entry for a conversation the App can reach or, as a fallback, a Human saved-target Service Grant. A SQLite transaction covers local facts only. External effects use an Outbox Intent, a stable idempotency identity, and bounded reconciliation without claiming exactly-once delivery. An outcome that cannot be proven becomes `unknown-outcome` for Human resolution. Wake Policy decides when an Orchestrator observes new attention: at the safe boundary after the current step, after the current turn, or by starting a new turn while idle. Ordinary external messages do not interrupt a running model/tool step. Only a DSH-supported and policy-authorized control path may steer execution. Ready attention is consumed by turn, not by event: while a turn runs, arrivals mark a ready set, and one harvest turn consumes it when the turn ends or the Bot is idle; only direct mentions and DMs steer into the running turn (ADR-0077). Wake handling follows the Source class rather than the platform: external providers normalize into Source Events at the Bridge, and the runtime branches only on Source class and admission reason (ADR-0075). +### Bound apps admit default traffic (ADR-0142) + +Reception is keyed by the external identity Binding, not by a saved target. An enabled Binding admits its App's default traffic, direct messages and qualified group mentions, into the bound Bot's Inbox. The first admitted message from a conversation commits an implicit conversation entry (a `messaging_grants` row with `origin: 'implicit'`) in the same transaction as its Source Event and Inbox Admission, so Outbox fences, group policies, Channel Bridge routes and revocation keep one anchor. The Binding's new-conversation mode (`auto` or `ask`) and its bounds (20 new entries per hour, 500 active, 200 held) decide whether a new conversation is admitted or held. A block is durable per Bot, App fingerprint, conversation kind and ID; mute moves only `preferenceRevision`, leaving the entry's Outbox revision untouched. A Bot may bind several Apps of one platform; each App still belongs to one Bot. In the Client the App's conversation list manages entries and shows their existing Channel Bridge routes (creating a sync is being redesigned around External connectors), and the External connectors entry lists those routes and keeps saved-target Service Grants as the advanced fallback for Providers that can't list or address conversations. + ### Local Human names — target design (ADR-0103) The local Human has one optional default display name within the DSH Profile, edited in BotHarness plugin settings and falling back to `Human`. Every Human-participating DM or Group Channel may override it with **Human Channel nickname**, edited through “My nickname” in the Channel header menu. Clearing an override restores inheritance; changing the default affects only Channels without an override. These names support roleplay with different partners without adding saved roleplay backgrounds, another Human account, or another Human Inbox. diff --git a/docs/architecture/botharness-architecture.md b/docs/architecture/botharness-architecture.md index ebf492b007..cbd835f4db 100644 --- a/docs/architecture/botharness-architecture.md +++ b/docs/architecture/botharness-architecture.md @@ -227,10 +227,14 @@ sequenceDiagram M->>DB: append attempt and outcome facts ``` -Source Event 是内容唯一权威;Channel 和 Inbox 都只保存关系。Reply 使用可信 Reply Route 自动选择来源 provider;主动发布属于 Service Action,必须同时满足 Provider Capability 与 Human Service Grant。SQLite 事务只覆盖本地事实;外部副作用使用 Outbox Intent、幂等标识和有界 reconciliation,不宣称 exactly-once。不可证明的结果进入 `unknown-outcome`,由 Human 处理。 +Source Event 是内容唯一权威;Channel 和 Inbox 都只保存关系。Reply 使用可信 Reply Route 自动选择来源 provider;主动发布属于 Service Action,需要 Provider Capability,以及该 Bot 对应用可触达会话的会话条目,或作为后备的 Human 保存目标 Service Grant。SQLite 事务只覆盖本地事实;外部副作用使用 Outbox Intent、幂等标识和有界 reconciliation,不宣称 exactly-once。不可证明的结果进入 `unknown-outcome`,由 Human 处理。 Wake Policy 决定何时让 Orchestrator 看见新 attention:当前 step 完成后的安全边界、当前 turn 结束后,或 idle 时启动新 turn。普通外部消息不打断正在执行的 model/tool step;只有 DSH 明确支持且策略授权的控制路径才能 steer。就绪的 attention 按回合收割:忙碌期间新到的事件只把就绪集合置脏,当前回合结束(即空闲)时由一次 harvest turn 消费全部就绪项;主动 steer 只用于直接 @ 与 DM(ADR-0077)。 +### 绑定的应用接收默认流量(ADR-0142) + +接收以外部身份 Binding 为键,而不是保存的目标。启用的 Binding 把应用的默认流量(私聊和合格的群 @)接收进绑定 Bot 的 Inbox。会话的第一条被接收的消息,会与其 Source Event、Inbox Admission 在同一事务中提交一个隐式会话条目(`messaging_grants` 中 `origin: 'implicit'` 的行),使 Outbox fence、群策略、Channel Bridge 路由和撤销仍挂在同一个锚点上。Binding 的新会话模式(`auto` 或 `ask`)及其上限(每小时 20 个新条目、500 个活跃、200 个等待)决定新会话被接收还是等待处理。屏蔽按 Bot、应用指纹、会话类型和 ID 持久保存;静音只推进 `preferenceRevision`,不改动条目的 Outbox revision。一个 Bot 可以绑定同一平台的多个应用,每个应用仍只属于一个 Bot。Client 中,应用的会话列表管理这些条目并显示已有的 Channel Bridge 路由(新建同步正在围绕外部连接器重新设计),「外部连接器」列出这些路由并保留保存目标的 Service Grant,作为 Provider 无法列出或直接发往会话时的高级后备。 + [#824](https://github.com/BotHarness/BotHarness/issues/824) 在 PersonaBot Profile 增加可恢复的配置引导,本地打包的 Driver.js 定位既有原生 IM 设置、身份与群授权控件。进度只投影当前兼容 Provider、账号、Binding/Grant、已收测试 Source Event 和沿原来源回复的 Outbox 回执;可选的认证自身回传仍单独呈现。平台接受、外部可见投递和已读回执明确区分。不建立新手引导数据库、额外凭据存储或消息权威;打开和关闭引导不产生授权或发送。 ### 首条外部出站路径(ADR-0101) diff --git a/docs/assets/pr/1113-connectors-relabel/README.md b/docs/assets/pr/1113-connectors-relabel/README.md new file mode 100644 index 0000000000..fa1bebe8c3 --- /dev/null +++ b/docs/assets/pr/1113-connectors-relabel/README.md @@ -0,0 +1,24 @@ +# #1113 External connectors relabel + +Captured from a real DSH 0.2.0-rc.1 instance with the QA fake IM provider and the real OpenCode Go model. + +## Conversation rows: actions on their own row, a tooltip on each + +| | Light | Dark | +| --- | -------------------------- | ------------------------- | +| zh | ![](rows-v4-zh-light.webp) | ![](rows-v4-zh-dark.webp) | +| en | ![](rows-v4-en-light.webp) | ![](rows-v4-en-dark.webp) | + +## External connectors: syncs plus the advanced send target + +| | Light | Dark | +| --- | ------------------------ | ----------------------- | +| zh | ![](conns-zh-light.webp) | ![](conns-zh-dark.webp) | +| en | ![](conns-en-light.webp) | ![](conns-en-dark.webp) | + +## Lark setup guide: three steps + +| | Light | Dark | +| --- | --------------------------- | -------------------------- | +| zh | ![](guide-v2-zh-light.webp) | ![](guide-v2-zh-dark.webp) | +| en | ![](guide-v2-en-light.webp) | ![](guide-v2-en-dark.webp) | diff --git a/docs/assets/pr/1113-connectors-relabel/conns-en-dark.webp b/docs/assets/pr/1113-connectors-relabel/conns-en-dark.webp new file mode 100644 index 0000000000..1148782f68 Binary files /dev/null and b/docs/assets/pr/1113-connectors-relabel/conns-en-dark.webp differ diff --git a/docs/assets/pr/1113-connectors-relabel/conns-en-light.webp b/docs/assets/pr/1113-connectors-relabel/conns-en-light.webp new file mode 100644 index 0000000000..5903ed7579 Binary files /dev/null and b/docs/assets/pr/1113-connectors-relabel/conns-en-light.webp differ diff --git a/docs/assets/pr/1113-connectors-relabel/conns-zh-dark.webp b/docs/assets/pr/1113-connectors-relabel/conns-zh-dark.webp new file mode 100644 index 0000000000..629271a263 Binary files /dev/null and b/docs/assets/pr/1113-connectors-relabel/conns-zh-dark.webp differ diff --git a/docs/assets/pr/1113-connectors-relabel/conns-zh-light.webp b/docs/assets/pr/1113-connectors-relabel/conns-zh-light.webp new file mode 100644 index 0000000000..feea96edf5 Binary files /dev/null and b/docs/assets/pr/1113-connectors-relabel/conns-zh-light.webp differ diff --git a/docs/assets/pr/1113-connectors-relabel/guide-v2-en-dark.webp b/docs/assets/pr/1113-connectors-relabel/guide-v2-en-dark.webp new file mode 100644 index 0000000000..6063f1cf50 Binary files /dev/null and b/docs/assets/pr/1113-connectors-relabel/guide-v2-en-dark.webp differ diff --git a/docs/assets/pr/1113-connectors-relabel/guide-v2-en-light.webp b/docs/assets/pr/1113-connectors-relabel/guide-v2-en-light.webp new file mode 100644 index 0000000000..3f55520947 Binary files /dev/null and b/docs/assets/pr/1113-connectors-relabel/guide-v2-en-light.webp differ diff --git a/docs/assets/pr/1113-connectors-relabel/guide-v2-zh-dark.webp b/docs/assets/pr/1113-connectors-relabel/guide-v2-zh-dark.webp new file mode 100644 index 0000000000..b5c3bae019 Binary files /dev/null and b/docs/assets/pr/1113-connectors-relabel/guide-v2-zh-dark.webp differ diff --git a/docs/assets/pr/1113-connectors-relabel/guide-v2-zh-light.webp b/docs/assets/pr/1113-connectors-relabel/guide-v2-zh-light.webp new file mode 100644 index 0000000000..7ba97e3a07 Binary files /dev/null and b/docs/assets/pr/1113-connectors-relabel/guide-v2-zh-light.webp differ diff --git a/docs/assets/pr/1113-connectors-relabel/rows-v4-en-dark.webp b/docs/assets/pr/1113-connectors-relabel/rows-v4-en-dark.webp new file mode 100644 index 0000000000..5d551cf8f8 Binary files /dev/null and b/docs/assets/pr/1113-connectors-relabel/rows-v4-en-dark.webp differ diff --git a/docs/assets/pr/1113-connectors-relabel/rows-v4-en-light.webp b/docs/assets/pr/1113-connectors-relabel/rows-v4-en-light.webp new file mode 100644 index 0000000000..50b7127a56 Binary files /dev/null and b/docs/assets/pr/1113-connectors-relabel/rows-v4-en-light.webp differ diff --git a/docs/assets/pr/1113-connectors-relabel/rows-v4-zh-dark.webp b/docs/assets/pr/1113-connectors-relabel/rows-v4-zh-dark.webp new file mode 100644 index 0000000000..7f64380d01 Binary files /dev/null and b/docs/assets/pr/1113-connectors-relabel/rows-v4-zh-dark.webp differ diff --git a/docs/assets/pr/1113-connectors-relabel/rows-v4-zh-light.webp b/docs/assets/pr/1113-connectors-relabel/rows-v4-zh-light.webp new file mode 100644 index 0000000000..5d21ae75c9 Binary files /dev/null and b/docs/assets/pr/1113-connectors-relabel/rows-v4-zh-light.webp differ diff --git a/docs/channel-sidebar/external-connectors.md b/docs/channel-sidebar/external-connectors.md index 011876fd51..14a0dfcd27 100644 --- a/docs/channel-sidebar/external-connectors.md +++ b/docs/channel-sidebar/external-connectors.md @@ -1,45 +1,34 @@ # External connectors -Open **Bot DM → Channel sidebar → External connectors**. An external connector decides which messages from an authorized external conversation enter this Bot: either this DM’s history or **Bot Inbox only**. It is managed separately from the identity the Bot speaks as; bind that first under [External identities](/docs/channel-sidebar/external-identities). +Open **Bot DM → Channel sidebar → External connectors**. It lists the external conversations synced into this Bot’s Channels and keeps one advanced action. You don’t need it to receive or reply: [binding an app](/docs/channel-sidebar/external-identities) is enough, and its conversations are managed there. Later, one-way input streams such as webhooks will also live here. -![The External connectors entry with connector rows and the authorization row](/guides/channel-sidebar/20-external-connectors-zh.webp) +![The External connectors entry with a synced conversation and the advanced send target row](/guides/channel-sidebar/20b-external-connectors-en.webp) ## Read the rows -| Row | What it shows and does | -| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| One row per connector | Connector name, a state chip (**Receiving**, **Connecting**, **Paused** or **Unavailable**; plus **Bot Inbox only** when it does not add DM history) and “platform · external conversation · condition”. The Switch pauses or resumes intake. | -| **+ Add connector** | Route an already authorized conversation into this DM or Bot Inbox only. | -| **Authorize a conversation** | Opens **Conversation authorization**. Once a target is authorized, the row shows its name, status chip and “platform · account”. | +| Row | What it shows and does | +| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| One row per sync | Sync name, a state chip (**Receiving**, **Connecting**, **Paused** or **Unavailable**; plus **Bot Inbox only** when it does not add DM history) and “platform · external conversation · condition”. The Switch pauses or resumes intake. | +| **Save a send target (advanced)** | Only for apps that can’t list or address conversations: save one target the Bot can post to proactively. Receiving and replying don’t need it. | -**Connectors decide what is received; the Bot’s [Wake policy](/docs/channel-sidebar/wake-policy) decides when it is processed.** +When nothing is synced, the entry says **No external sources connected**. Adding a sync here, from any connected app whether or not a Bot has it bound, is being redesigned. -## Authorize a conversation +**Syncs decide what is received; the Bot’s [Wake policy](/docs/channel-sidebar/wake-policy) decides when it is processed.** -The three steps: bind an identity under [External identities](/docs/channel-sidebar/external-identities) (who the Bot speaks as), authorize a specific external conversation (which group or DM it may send to and receive from), then optionally add a connector (where its messages land). +## Edit, pause or remove a sync -The conversations you can pick come from the platform side: grant the app its permissions in the platform console, add the bot to the group, then save a **Destination** in the account's delivery settings under **Settings → IM Bots**. A message arriving from a conversation does **not** create an authorization; messages from an unauthorized conversation stay out of the Bot Inbox even when they @ the bot. Authorization is only made here, by hand, and can be changed or revoked later. +Click a row to open **Edit connector**. Change the name, the condition (inherit, mentions of the receiving identity only, or all ordinary text messages) or the enable switch, then click **Save connector**. **Delete** asks for confirmation. Pausing keeps the configuration and history; deleting does not remove messages already received. -1. Click **Authorize a conversation**. -2. Choose the bound **IM account** and the saved **Destination**, then click **Bind and authorize destination**. Check the group, account and local receive target. -3. In the same dialog you can set the **Local receive target**, collection and wake for the group, Thread following, send a message to the target and inspect **Recent sends**. -4. **Refresh** reloads the current state. **Revoke this target authorization** removes the authorization. - -![The Conversation authorization dialog](/guides/channel-sidebar/22-conversation-authorization-zh.webp) - -## Add a connector - -1. Click **+ Add connector**. -2. Choose an authorized source. If the list is empty, authorize a conversation first. -3. Choose the destination: this DM’s history or **Bot Inbox only**. -4. Enter a recognizable **Connector name**, choose the condition and save. +![Editing an external connector](/guides/channel-sidebar/21-external-connector-dialog-zh.webp) -Adding a connector does not create an external account or new authorization. All dropdowns in these dialogs are searchable: type to filter. +## Advanced: save a send target -## Edit, pause or delete +Lark, Slack and Discord apps post to conversations directly, so they never need this. For an app whose Provider can’t list or address conversations: -Click a connector row to open **Edit connector**. Change the **Connector name**, the condition (inherit, mentions of the receiving identity only, or all ordinary text messages) or the enable switch, then click **Save connector**. **Delete** asks for confirmation (**Delete this connector**). Pausing keeps the configuration and history; deleting does not remove messages already received. +1. Save a **Destination** in the account’s delivery settings under **Settings → IM Bots**. +2. Click **Save a send target (advanced)**, choose the bound **IM account** and the saved **Destination**, and confirm. +3. In the same dialog you can send a message to the target and inspect **Recent sends**. **Revoke this target authorization** removes it. -![Editing an external connector](/guides/channel-sidebar/21-external-connector-dialog-zh.webp) +![The send target dialog](/guides/channel-sidebar/22-conversation-authorization-zh.webp) A local Group still manages its connectors in the group’s detailed Profile. Platform setup: [Lark / Feishu](/docs/lark-connection), [Slack](/docs/slack-connection), [personal WeChat](/docs/wechat-connection). [All sidebar features](/docs/channel-sidebar). diff --git a/docs/channel-sidebar/external-connectors.zh.md b/docs/channel-sidebar/external-connectors.zh.md index 61681d7105..7a46e6b78e 100644 --- a/docs/channel-sidebar/external-connectors.zh.md +++ b/docs/channel-sidebar/external-connectors.zh.md @@ -1,45 +1,34 @@ # 外部连接器 -打开 **Bot 私聊 → Channel sidebar → 外部连接器**。外部连接器决定已授权外部会话中的哪些消息进入这个 Bot:进入本私聊的消息历史,或 **仅 Bot Inbox**。它与 Bot 的发言身份分开管理;请先在[外部身份](/zh/docs/channel-sidebar/external-identities)中绑定身份。 +打开 **Bot 私聊 → Channel sidebar → 外部连接器**。这里列出同步到这个 Bot 的 Channel 的外部会话,并保留一个高级操作。收消息和回复都不需要它:[绑定应用](/zh/docs/channel-sidebar/external-identities)即可,会话也在那里管理。以后,Webhook 等单向输入流也会放在这里。 -![外部连接器分区,包含连接器行与授权行](/guides/channel-sidebar/20-external-connectors-zh.webp) +![外部连接器分区:一个已同步的会话和高级发送目标行](/guides/channel-sidebar/20b-external-connectors-zh.webp) ## 读懂每一行 -| 行 | 显示与作用 | -| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 每个连接器一行 | 连接器名称、状态标签(**正在收件**、**正在连接**、**已暂停** 或 **暂不可用**;不添加私聊历史时另有 **仅 Bot Inbox**),以及「平台 · 外部会话 · 接收条件」。右侧 Switch 暂停或恢复收件。 | -| **+ 添加外部连接器** | 把已授权的会话接入本私聊或仅 Bot Inbox。 | -| **授权外部会话** | 打开 **外部会话授权**。已授权目标后,这一行显示目标名称、状态标签和「平台 · 账号」。 | +| 行 | 显示与作用 | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 每个同步一行 | 同步名称、状态标签(**正在收件**、**正在连接**、**已暂停** 或 **暂不可用**;不添加私聊历史时另有 **仅 Bot Inbox**),以及「平台 · 外部会话 · 接收条件」。右侧 Switch 暂停或恢复收件。 | +| **保存发送目标(高级)** | 仅用于不能列出或直接发往会话的应用:保存一个目标,让 Bot 主动发消息。收消息和回复不需要它。 | -**连接器决定收什么,Bot 的[唤醒策略](/zh/docs/channel-sidebar/wake-policy)决定何时处理。** +没有同步时,这里显示 **尚未接入外部信息**。在这里新建同步(任意已连接应用,无论是否绑定了 Bot)的方式正在重新设计。 -## 授权外部会话 +**同步决定收什么,Bot 的[唤醒策略](/zh/docs/channel-sidebar/wake-policy)决定何时处理。** -三步的关系:先在[外部身份](/zh/docs/channel-sidebar/external-identities)绑定身份(Bot 以谁发言),再授权具体外部会话(可以收发哪个群或私聊),最后可选添加外部连接器(消息进入哪里)。 +## 编辑、暂停或移除同步 -可选的会话来自平台侧:在平台后台给应用开好权限、把机器人拉进群,再在 **设置 → IM机器人** 的账号投递设置中保存 **发送目标**。收到某个会话的消息**不会**自动生成授权;未授权的会话即使 @ 机器人,消息也不会进入 Bot Inbox。授权只能在这里手动完成,之后可以修改或撤销。 +点击一行,打开 **编辑外部连接器**。修改名称、**接收条件**(继承、仅收 @ 接收身份、所有普通文字消息)或启用开关,然后点击 **保存外部连接器**。**删除** 需要确认。暂停会保留配置与历史;删除不会删除已收到的消息。 -1. 点击 **授权外部会话**。 -2. 选择已绑定的 **IM账号** 和保存的 **发送目标**,点击 **绑定并授权此目标**。核对群名称、账号和本地收件位置。 -3. 在同一弹窗中,还可以设置 **本地收件位置**、此群的收件与唤醒、话题跟进,向目标发送消息,并查看 **最近发送**。 -4. **刷新** 重新读取当前状态;**撤销此目标授权** 移除授权。 - -![外部会话授权弹窗](/guides/channel-sidebar/22-conversation-authorization-zh.webp) - -## 添加外部连接器 - -1. 点击 **+ 添加外部连接器**。 -2. 选择 **已授权来源**。列表为空时,先授权外部会话。 -3. 选择投递目标:本私聊的消息历史,或 **仅 Bot Inbox**。 -4. 填写易识别的 **连接器名称**,选择接收条件后保存。 +![编辑外部连接器弹窗](/guides/channel-sidebar/21-external-connector-dialog-zh.webp) -添加连接器不会创建外部账号或新增授权。这些弹窗中的下拉框都支持搜索,输入文字即可筛选。 +## 高级:保存发送目标 -## 编辑、暂停或删除 +Lark、Slack 和 Discord 应用可以直接发往会话,永远不需要这一步。对于 Provider 不能列出或直接发往会话的应用: -点击连接器行,打开 **编辑外部连接器**。修改 **连接器名称**、**接收条件**(继承、仅收 @ 接收身份、所有普通文字消息)或启用开关,然后点击 **保存外部连接器**。**删除** 需要确认(**删除此连接器**)。暂停会保留配置与历史;删除不会删除已收到的消息。 +1. 在 **设置 → IM机器人** 的账号投递设置中保存 **发送目标**。 +2. 点击 **保存发送目标(高级)**,选择已绑定的 **IM账号** 和保存的 **发送目标** 并确认。 +3. 在同一弹窗中,还可以向目标发送消息并查看 **最近发送**;**撤销此目标授权** 会移除它。 -![编辑外部连接器弹窗](/guides/channel-sidebar/21-external-connector-dialog-zh.webp) +![发送目标弹窗](/guides/channel-sidebar/22-conversation-authorization-zh.webp) 本地群聊仍在群的详细 Profile 中管理外部连接器。平台配置:[Lark / 飞书](/zh/docs/lark-connection)、[Slack](/zh/docs/slack-connection)、[个人微信](/zh/docs/wechat-connection)。返回[侧栏总览](/zh/docs/channel-sidebar)。 diff --git a/docs/channel-sidebar/external-identities.md b/docs/channel-sidebar/external-identities.md index f76ab27ee4..44e25312dd 100644 --- a/docs/channel-sidebar/external-identities.md +++ b/docs/channel-sidebar/external-identities.md @@ -14,7 +14,7 @@ The same goes for Slack and Discord: DM the app or @mention it in a channel it b | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | One row per bound app | Local display name, a status chip (**Available**, **Paused**, **Unavailable** or **Rebind required**) and “platform · N conversation(s)”. The Switch pauses the app. | | **+ Bind app** | Bind a connected app to this Bot. | -| **Connect Lark / Feishu** (Setup guide) | Opens the step-by-step Lark / Feishu setup guide. Its **Locate** buttons highlight the matching sidebar row, expanding **External connectors** when needed. | +| **Connect Lark / Feishu** (Setup guide) | Opens the three-step Lark / Feishu setup guide: connect the app, bind it, send a test message. **Locate** highlights the matching control. | | **IM administrator pairing** | Shows “N pending” and “N paired”. Opens the pairing review dialog; see [Connect a Bot to Lark / Feishu](/docs/lark-connection). | | **Lark approval notifications** | Shows the destination, or **Automatic notifications off**. Opens the approval notification dialog. | @@ -44,7 +44,8 @@ The first DM or @mention that reaches the Inbox records its conversation. Click | **Muted** | Messages still reach the Inbox but never wake the Bot; you can still reply | **Unmute**, **Rules** (groups), **Block** | | **Blocked** | Messages are dropped and the conversation never comes back on its own | **Allow again** | -- **Rules** opens the same collection and wake settings as a group in External connectors, for this one group. +- **Rules** opens collection and wake settings for this one group. +- Hover any action to see what it does. A conversation that is already synced into a Channel shows **Synced to …**; manage syncs under [External connectors](/docs/channel-sidebar/external-connectors). - **Block** asks for confirmation. Replies that haven’t been sent yet are cancelled, and old messages can no longer be replied to. The block stays across restarts and even after you unbind and bind the same app to this Bot again. - **Allow again** starts a fresh conversation from the next message. Messages sent while it was blocked are not backfilled, and its old rules are not restored. diff --git a/docs/channel-sidebar/external-identities.zh.md b/docs/channel-sidebar/external-identities.zh.md index f6ade474d6..c102e9c8e8 100644 --- a/docs/channel-sidebar/external-identities.zh.md +++ b/docs/channel-sidebar/external-identities.zh.md @@ -14,7 +14,7 @@ Slack 和 Discord 同样适用:私聊应用、或在它所在的频道里 @ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | 每个已绑定应用一行 | 本地显示名称、状态标签(**可用**、**已暂停**、**暂不可用** 或 **需要重新绑定**),以及「平台 · N 个会话」。右侧 Switch 启用或暂停这个应用。 | | **+ 绑定应用** | 选择一个已连接的应用,绑定给这个 Bot。 | -| **连接 Lark / 飞书**(配置引导) | 打开 Lark / 飞书分步配置引导。其中的 **定位** 会高亮侧栏中对应的行,必要时自动展开 **外部连接器**。 | +| **连接 Lark / 飞书**(配置引导) | 打开三步的 Lark / 飞书配置引导:连接应用、绑定、发消息验证。**定位** 会高亮对应控件。 | | **IM 管理员配对** | 显示「N 个待处理」和「已配对 N 人」,点击打开配对审核弹窗;见[连接 Lark / 飞书](/zh/docs/lark-connection)。 | | **Lark 审批通知** | 显示通知目的地,或 **关闭自动通知**;点击打开审批通知弹窗。 | @@ -44,7 +44,8 @@ Slack 和 Discord 同样适用:私聊应用、或在它所在的频道里 @ | **已静音** | 消息照常进入收件箱,但不唤醒 Bot;仍可回复 | **取消静音**、**规则**(群)、**屏蔽** | | **已屏蔽** | 消息直接丢弃,会话不会自己回来 | **再次允许** | -- **规则** 打开与「外部连接器」中群相同的收件和唤醒设置,只作用于这一个群。 +- **规则** 打开这一个群的收件和唤醒设置。 +- 鼠标悬停在操作按钮上可以看到说明。已同步到 Channel 的会话会显示 **已同步到 …**;同步在[外部连接器](/zh/docs/channel-sidebar/external-connectors)中管理。 - **屏蔽** 需要确认。尚未发出的回复会取消,旧消息也不能再回复。屏蔽在重启后仍然有效,即使解绑后把同一个应用重新绑定到这个 Bot 也一样。 - **再次允许** 从下一条消息开始重新接收。屏蔽期间的消息不会补收,旧的规则也不会恢复。 diff --git a/docs/channel-sidebar/index.md b/docs/channel-sidebar/index.md index e33674cce7..9b94082806 100644 --- a/docs/channel-sidebar/index.md +++ b/docs/channel-sidebar/index.md @@ -26,7 +26,7 @@ These pages use DSH **0.2.0-rc.1** and the public **deepseekbot** package. First | [Model](/docs/channel-sidebar/model) | Check and change the models this Bot uses for chat and new Assignments. | | [Wake policy](/docs/channel-sidebar/wake-policy) | Choose, per source, whether this Bot wakes right away, after a digest, or not. | | [External identities](/docs/channel-sidebar/external-identities) | Bind who this Bot speaks as on Lark / Feishu, Slack or WeChat; IM pairing and approval notifications. | -| [External connectors](/docs/channel-sidebar/external-connectors) | Authorize external conversations and choose which messages reach this DM or Bot Inbox. | +| [External connectors](/docs/channel-sidebar/external-connectors) | Edit and pause conversations synced into a Channel; advanced saved send targets. | | In a local group | What you can do | | ------------------------------------------------------------ | ----------------------------------------------------------------------- | diff --git a/docs/channel-sidebar/index.zh.md b/docs/channel-sidebar/index.zh.md index bf4fc2f7c6..d2931f5f30 100644 --- a/docs/channel-sidebar/index.zh.md +++ b/docs/channel-sidebar/index.zh.md @@ -26,7 +26,7 @@ | [模型](/zh/docs/channel-sidebar/model) | 查看并修改这个 Bot 日常对话与新任务使用的模型。 | | [唤醒策略](/zh/docs/channel-sidebar/wake-policy) | 按来源设置这个 Bot 立即唤醒、汇总后唤醒或只记录。 | | [外部身份](/zh/docs/channel-sidebar/external-identities) | 绑定这个 Bot 在 Lark / 飞书、Slack、微信的发言身份;IM 管理员配对与审批通知。 | -| [外部连接器](/zh/docs/channel-sidebar/external-connectors) | 授权外部会话,选择哪些消息进入本私聊或 Bot 收件箱。 | +| [外部连接器](/zh/docs/channel-sidebar/external-connectors) | 修改、暂停同步到 Channel 的会话;高级的保存发送目标。 | | 本地群聊中的项目 | 可以做什么 | | ----------------------------------------------- | ---------------------------------------------- | diff --git a/docs/lark-connection.md b/docs/lark-connection.md index a2a1de2431..819b30425d 100644 --- a/docs/lark-connection.md +++ b/docs/lark-connection.md @@ -2,21 +2,19 @@ Install the plugin first using the [illustrated installation guide](/docs/installation), then return here after a local Bot DM works. -Connect a PersonaBot to a work group: connect the application bot, bind its identity and authorize a group, then choose whether incoming messages belong in the Bot Inbox or a local Channel. +Connect a PersonaBot to Lark in three steps: connect the application bot, bind it to the Bot, and send a test message. After binding, DMs to the app and @mentions of it in its groups reach this Bot’s Inbox, and the Bot replies in place. You don’t save a delivery target or authorize each conversation. -This guide starts with **Lark international, an application bot, a test group and mentions-only intake**. Feishu uses the same sequence, with a Feishu application and the Feishu platform selected. +This guide starts with **Lark international, an application bot and a test group**. Feishu uses the same sequence, with a Feishu application and the Feishu platform selected. ## Understand the three settings -| Setting | Location | Purpose | -| ------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------- | -| IM application connection | Settings → IM Bots → Feishu | Connect this Host to a Lark / Feishu application bot | -| External identity | Bot DM → Channel sidebar → External identities | Choose who this Bot speaks as externally | -| External connector | Bot DM → External connectors, or a Group Channel's detailed Profile | Choose which external group and messages enter this Channel | +| Setting | Location | Purpose | +| ------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| IM application connection | Settings → IM Bots → Feishu (overview: Settings → IM apps) | Connect this Host to a Lark / Feishu application bot | +| External identity | Bot DM → Channel sidebar → External identities → **Bind app** | Bind the app to this Bot, then manage its conversations: mute, rules, block, sync to a Channel | +| External connector | Bot DM → Channel sidebar → External connectors | Advanced: edit Channel syncs and save send targets for apps that can’t address conversations | -**Once the app is bound, DMs and @mentions just work.** Under External identities, click **+ Bind app** and pick this Lark app. DMs to it and @mentions of it in groups it belongs to then go straight to this Bot’s Inbox, and the Bot replies in place, with no saved delivery target, conversation authorization or reception switch. See [External identities](/docs/channel-sidebar/external-identities). - -Saved targets and group authorization in sections 3 and 4 below still apply when you want unmentioned group messages, a local Channel for the group, or proactive posts from the Bot. An external connector does not lend the app to other Bots. +Where the Bot can speak is decided by the platform: the groups the app was added to and the permissions you published. An app belongs to one Bot; binding it does not lend it to other Bots. This guide combines actual setup controls with a Profile that completed real Lark connection. Configured captures come from the #823 qualified product (`0.0.0-test.823.6` / Provider `4.32.0-botharness.2`), using the dedicated “BotHarness IM QA #78” group. Empty forms show where to enter values; they are not proof of connection. No App Secret appears in the media. Use the browser image menu to view the original size. @@ -70,7 +68,7 @@ This is a **step-by-step edit of actual UI screenshots**, not an uninterrupted r [Download video](/guides/lark/lark-setup-walkthrough.mp4) · [English captions](/guides/lark/lark-setup.en.vtt) · [中文字幕](/guides/lark/lark-setup.zh.vtt) -Chapters: 0:00 App credentials and permissions → 0:32 Local connection → 0:48 Events and publishing → 1:12 Delivery target → 1:20 Identity and group authorization → 1:36 Message source. The executable steps remain below. +Chapters: 0:00 App credentials and permissions → 0:32 Local connection → 0:48 Events and publishing → 1:12 Delivery target → 1:20 Identity and group authorization → 1:36 Message source. The video records the earlier flow, which saved a delivery target and authorized the group before binding; binding the app is now enough. The executable steps remain below. ## 2. Prepare an application bot @@ -90,7 +88,7 @@ Enable permissions by purpose: - `im:message.group_msg`: obtain all group messages; needed for ordinary-message intake, group history and topic following. Request this sensitive permission when needed. - `im:resource`: upload images and files; needed when sending attachments. -Your organization determines availability and approval requirements. Even with all-group-message permission, BotHarness only processes explicitly authorized groups and configured intake conditions. See the official [message receive event](https://open.larksuite.com/document/server-docs/im-v1/message/events/receive) and [message history API](https://open.larksuite.com/document/server-docs/im-v1/message/get-2). +Your organization determines availability and approval requirements. Even with all-group-message permission, BotHarness only takes ordinary messages from groups whose **Rules** allow them; mentions and DMs are admitted on their own. See the official [message receive event](https://open.larksuite.com/document/server-docs/im-v1/message/events/receive) and [message history API](https://open.larksuite.com/document/server-docs/im-v1/message/get-2). ### Compare the actual console configuration @@ -141,92 +139,63 @@ _Figure 1: Select international Lark for a Lark application. Enter secrets only ![Actual test application connected to the Provider](/guides/lark/06-connected.webp) -_Figure 1b: A green online status (运行正常 in Chinese) proves transport connectivity. Identity binding, group authorization and correlated message/reply verification are separate checks._ - -Next, configure a **delivery target** for this account: - -1. Open the connected bot's settings, then **Delivery targets → New target**. -2. Choose the test group from **Choose from conversations you have chatted in**, checking its name and Chat ID; alternatively, enter its native platform group ID (`oc_…`) manually. A group name or message ID is not a Chat ID. -3. Set a call alias such as `lark-test-group`, click **Test**, confirm the test message in Lark, then **Save target**. - -A fresh application may have no conversation suggestions. In the #824 test, a preliminary @mention did not immediately populate this list and used the Provider’s standalone Session before PersonaBot binding. Enter a verified native group Chat ID manually, then Test and Save; ask your group administrator for the ID if needed. A standalone reply or discovery message is not canonical BotHarness receipt. - -![Actual saved group delivery target](/guides/lark/16-delivery-target.webp) - -_Account settings → Delivery settings → Delivery targets. Testing and saving a target checks the sending destination; PersonaBot identity binding and authorization come next._ - -## 4. Bind a PersonaBot identity and authorize the group - -In **Bot mode**, open the intended Bot DM and use the **Channel sidebar** on the right. Under **External identities**, click **Connect Lark / Feishu** (Setup guide), then select the application platform, IM account and saved group target. Each group is verified separately; another group’s successful reply cannot verify this group. **Locate** highlights the matching sidebar row, expanding **External connectors** when needed; committed configuration and correlated receipt/reply determine completion. - -![Annotated identity and group setup: bind identity, authorize the specific group, then optionally add a Channel connector](/guides/lark/05-identity-routing-annotated.webp) +_Figure 1b: A green online status (运行正常 in Chinese) proves transport connectivity. Binding the app to a Bot and a correlated message/reply are separate checks._ -_Walkthrough: 1 Bind identity → 2 Authorize the specific group → 3 Optionally connect a Channel. This is an AI-assisted annotated composite of the screenshots, showing an unconnected demonstration account; the original UI screenshots remain below for comparison._ +The account is ready for binding. You don’t need a delivery target to receive or reply; it is only needed for [saved send targets](#advanced-saved-send-targets). -1. Under **External identities → + Bind app**, select the connected application under **App** and click **Bind app**. Once it shows **Ready**, DMs and @mentions already reach the Inbox; continue with the authorization steps below only for unmentioned messages, a Channel or proactive sends. -2. Under **External connectors**, click **Authorize a conversation**. In the **Conversation authorization** dialog, select that IM account and the saved **send target**. -3. Click **Bind and authorize this target**. Check the group, account and local intake destination. -4. For the first test, retain **This Bot's Inbox only** and **mentions only**. This needs no additional local group and does not add messages to the Bot DM history. +## 4. Bind the app to a PersonaBot -![PersonaBot identity binding dialog before an IM account is configured](/guides/lark/02-identity.webp) +In **Bot mode**, open the intended Bot DM and use the **Channel sidebar** on the right. -_Screenshots in this section show the earlier Profile layout; these settings now live in the sidebar's External identities / External connectors entries._ +1. Under **External identities**, click **+ Bind app**, choose the connected application under **App** and confirm. One Bot can bind several apps, including several Lark apps; an app already bound to another Bot is not offered. +2. The identity row shows **Ready**: DMs to the app and @mentions of it in groups it belongs to now reach this Bot’s Inbox. +3. Choose how **New conversations** are admitted. **Admit automatically** (the default) adds each new DM or group on its first admitted message. **Ask me first** holds it under **Waiting** until you allow it. At most 20 new conversations are admitted per hour and 500 stay active per app; more wait for you. -_Figure 2: The identity belongs to the PersonaBot. If the selector is empty, check the application connection, Provider version and saved target. Human QR login is not Bot identity binding._ +![Bound Lark app with its conversation list grouped as Waiting, Active, Muted and Blocked](/guides/channel-sidebar/19c-app-conversations-en.webp) -![PersonaBot Profile before external identity and group authorization are configured](/guides/lark/03-authorize.webp) +_Each admitted DM or group appears in the app’s conversation list. **Mute** keeps the conversation but stops the Bot from being woken by it. **Rules** sets which group messages are received, for example ordinary messages without a mention. **Block** refuses the conversation durably until you click **Allow again**; nothing sent while blocked is backfilled._ -_Figure 3: Bind the identity, then authorize a specific group under External connectors → Authorize a conversation. The demonstration has no account yet, so authorization controls are unavailable._ +**Connect Lark / Feishu** (the setup guide in External identities) tracks the same three steps: connect the app, bind it to this Bot and send a test message. **Locate** highlights the matching control; steps are marked done only from real configuration and a correlated reply. -![Actual bound Lark identity and authorized group, using Inbox-only and mention-only collection](/guides/lark/07-granted.webp) +![The setup guide with three steps: connect the app, bind it, send a test message](/guides/lark/28-setup-guide.en.webp) -_Figure 3b: The real test group is authorized and reception is connected. Ordinary-message collection remains disabled; policy inheritance and collection scope are separate fields._ +## 5. Optional: sync a conversation into a local Channel -Each Bot can bind multiple platforms, with one identity per platform. It uses its own identity across local Channels. Other Bots in a shared Channel need their own identity and target authorization to reply externally. +Lark conversations reach the Bot Inbox by default. Adding a new sync from the app is being redesigned: **External connectors** will take a conversation from any connected app, whether or not a Bot has it bound, and stream it into a Channel as context. -## 5. Optional: display messages in a local Channel +Existing syncs keep working: edit, pause or remove them under **External connectors**. A sync uses the same authority as the conversation itself; it does not grant group access or lend the app to another Bot. A Channel can receive several sources, and a source can reach several Channels. **Syncs decide what is received; each member Bot’s Attention / wake policy decides when it is processed.** External messages show the source name above the bubble; clicking it opens details. Ordinary local replies are not broadcast to Lark. -Skip this section if you want Lark conversations to enter only the Bot Inbox. For a local team timeline, open an existing Group Channel's detailed Profile and add the receiving Bot as a member first. You can also route the source into a Bot DM's message history from that DM's sidebar: **External connectors → + Add connector**. +Adjust behaviour after the first successful test: -In a Group Channel's detailed Profile, use **External connectors → Add connector**: +- Manage global defaults in **Settings → Bot settings**. A group’s **Rules** can inherit or override them; changes affect future messages and do not import past history. +- For ordinary messages without mentions, publish `im:message.group_msg` and the subscription, send an unmentioned message in the group and refresh. Ordinary intake stays unavailable until genuine delivery is verified. +- Receiving unmentioned replies in a topic requires explicitly following that topic. -1. Select the **authorized Lark group**. This does not create an account or grant new group access; return to the preceding section if the list is empty. -2. Check the **delivery destination**: shared Channel, DM message history or Bot Inbox only. Choices depend on the current Channel. -3. Enter a recognizable **connector name**, such as “Lark team group.” -4. Retain **Inherit global defaults**, or explicitly choose **mentions of the receiving identity only**, enable intake and save. - -![Channel connector dialog with source, destination, name and intake condition](/guides/lark/04-connector.webp) - -_Screenshot shows the earlier Profile layout; in a Bot DM these settings now live in the sidebar's External connectors entry._ - -_Figure 4: Source, destination and intake condition are separate choices. Saving is unavailable in the demonstration until a real group is authorized._ +## 6. Verify the complete path -A Channel can receive several sources, and a source can route to several destinations. **Connectors decide what is received; each member Bot's Attention / wake policy decides when it is processed.** External messages show the source name above the bubble; clicking it opens details. Ordinary local replies are not automatically broadcast to Lark. +Start small: **mention the application bot** in a test group and send “Please reply here with LARK-OK.” Avoid testing several Bots at once. -Adjust advanced behavior after the first successful test: +1. The message appears in the Bot DM’s right-hand **Bot Inbox** with the correct Lark group, sender and content, and the group appears under **Active** in the app’s conversation list. +2. Source details show the external message ID, Source Event ID and topic information when present. +3. Lark receives `LARK-OK` from this Bot’s own app in the original conversation. A topic test replies in the same topic. +4. Send an ordinary unmentioned message and confirm it does not reach this Bot unless the group’s **Rules** allow ordinary messages. -- Manage global defaults in **Settings → Bot settings**. Profiles can inherit or override them; changes affect future messages and do not import past history. -- For ordinary messages without mentions, publish the required application permission and subscription, send an unmentioned message in the authorized test group, and refresh the verification result. All-message intake stays unavailable until genuine ordinary-message delivery is verified. -- Choose count/time harvest or per-message wake in group intake and wake settings. Inbox admission does not require an immediate reply; the Bot can decide whether to participate. -- Receiving unmentioned replies in a topic requires explicitly following that topic; replying once does not automatically follow it. The platform must actually deliver those events to the application. -- An external connector's Switch pauses intake while preserving configuration and history. Deleting it does not remove received messages. Unbinding an identity is separate and affects that Bot's external behavior using it. +**Lark’s green or gray read circle does not show whether a Bot received a message.** Use the local Inbox source record and the actual reply. -## 6. Verify the complete path +![Real source details, native message ID and Source Event ID](/guides/lark/08-source.webp) -Start with a small test: **mention the application bot** in the authorized Lark group and send “Please reply here with LARK-OK.” Avoid testing several Bots or routes at once. +_In the Bot DM sidebar, expand Bot Inbox → group; if the message is already handled, expand the processed/ignored section too. Click the message to open its Modal, then expand Source details and Message details._ -Confirm each result: +## Advanced: saved send targets -1. The message appears in the Bot DM's right-hand **Bot Inbox**, with the correct Lark group source, original sender and content. -2. Source details show the external message ID, Source Event ID and topic information when present. -3. Lark receives `LARK-OK` from this Bot's own application identity in the original conversation. A topic test should reply in the same topic. -4. With default mentions-only intake, send an ordinary unmentioned message and confirm it does not create ordinary intake for this Bot. Explicitly followed topics use their topic policy. +Lark lists and addresses conversations directly, so a Lark app never needs a saved target to receive, reply or post proactively. Saved send targets remain as a fallback for apps whose Provider can’t list or address conversations: -**Lark's green or gray read circle does not establish whether a Bot received a message.** Use the local Inbox source record and actual external reply. +1. In the connected bot’s settings, open **Delivery targets → New target**, choose or enter the conversation, click **Test** and **Save target**. +2. In the Bot DM sidebar, open **External connectors → Save a send target (advanced)**, choose the app and the saved target, and confirm. -![Real source details, native message ID and Source Event ID](/guides/lark/08-source.webp) +![Actual saved group delivery target](/guides/lark/16-delivery-target.webp) -_In the Bot DM sidebar, expand Bot Inbox → group; if the message is already handled, expand the processed/ignored section too. Click the message to open its Modal, then expand Source details and Message details. This example retains real topic and message identifiers; a platform ID appears if the sender name cannot be resolved._ +_Screenshot shows the earlier flow, where a target was required before binding._ ## 7. Request reviewed IM authority in a Lark DM @@ -301,16 +270,16 @@ These additional captures use the integrated source preview in a fresh isolated ## Troubleshooting -| Symptom | Check first | -| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Application missing or connection fails | Lark / Feishu selection, organization, App ID / Secret and running Host | -| Connected but no selectable IM account | Qualified Provider, online account, tested and saved target; refresh the sidebar | -| Group missing from authorized list | Bound Bot identity, saved delivery target, explicit authorization in that Bot's External connectors entry and membership in the shared Channel | -| Mention does not enter Inbox | Application bot in the group; application permissions, long-connection `im.message.receive_v1` subscription, published version and approval; local authorization and intake Switch | -| Ordinary or unmentioned topic messages do not arrive | `im:message.group_msg`, genuine event delivery verification, group intake condition and explicit topic following | -| History read returns 230027 | Effective published application group-message permission; Human login permission cannot substitute for it | -| Received but no reply | Working model, Inbox / wake state, this Bot's enabled identity and send authorization for the target | -| No message in local DM | Check for Inbox-only routing; it is a valid separate destination, not a lost message | +| Symptom | Check first | +| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Application missing or connection fails | Lark / Feishu selection, organization, App ID / Secret and running Host | +| Connected but the app is missing from Bind app | Qualified Provider, online account; an app already bound to another Bot is not offered (see Settings → IM apps) | +| Group missing from the conversation list | The app was added to the group, a first message @mentioned it, and the conversation isn't waiting (Ask me first or the hourly limit) or blocked | +| Mention does not enter Inbox | Application bot in the group; application permissions, long-connection `im.message.receive_v1` subscription, published version and approval; identity enabled, conversation not muted or blocked | +| Ordinary or unmentioned topic messages do not arrive | `im:message.group_msg`, genuine event delivery verification, group intake condition and explicit topic following | +| History read returns 230027 | Effective published application group-message permission; Human login permission cannot substitute for it | +| Received but no reply | Working model, Inbox / wake state, this Bot's enabled identity and `im:message:send_as_bot` | +| No message in local DM | Conversations reach the Bot Inbox by default; only an existing sync (see **External connectors**) shows them in a Channel | When requesting help, include the platform, reproduction steps, a public-safe error code and checks already performed. Do not include App Secrets, access tokens or unrelated group messages. @@ -336,7 +305,7 @@ _Confirm the test message in the designated group, then save the target._ ![Account, target, identity and authorization confirmed](/guides/lark/23-new-app-guide.webp) -_The first four steps derive from real configuration; tour clicks cannot mark them complete._ +_The earlier five-step guide; the current guide has three steps (app, bind, verify). Steps derive from real configuration; tour clicks cannot mark them complete._ ![Native topic mention and own-identity reply](/guides/lark/24-new-app-topic-reply.webp) @@ -380,8 +349,8 @@ In the earlier isolated 2026-10-07 test, the actual Lark platform accepted both In the image-capable #1021 candidate, an authorized Lark image or supported image-bearing post appears inside its original Channel bubble. The source name above it still opens source details. Images load when visible; select an image to enlarge it, and use **Retry** after a failed load. Text and multiple images stay in their native order in one message. -This requires an external connector with a history destination. Inbox-only reception does not place images in Channel history. Keep the application's existing message permissions: a mentions-only group source still requires a real Bot mention in a supported native post. This feature does not enable ordinary group-message access. +This requires the conversation to be synced to a Channel. Inbox-only reception does not place images in Channel history. Keep the application's existing message permissions: a mentions-only group source still requires a real Bot mention in a supported native post. This feature does not enable ordinary group-message access. -Pausing the connector keeps already acquired images readable but stops new image acquisition. Unbinding its identity or revoking source authorization makes that path unavailable, including cached images. A separately valid source path remains independent. Refresh and restart retain authorized acquired images; purged or missing originals are not downloaded again. Previews support PNG, JPEG, GIF and WebP up to 25 MiB. An unsupported format or oversized image shows an explicit state. +Pausing the sync keeps already acquired images readable but stops new image acquisition. Unbinding its identity or revoking source authorization makes that path unavailable, including cached images. A separately valid source path remains independent. Refresh and restart retain authorized acquired images; purged or missing originals are not downloaded again. Previews support PNG, JPEG, GIF and WebP up to 25 MiB. An unsupported format or oversized image shows an explicit state. Human image viewing does not make the Bot understand images or change its attention, model context or permissions. Real platform acceptance and screenshots for this candidate are tracked in [#1021](https://github.com/BotHarness/BotHarness/issues/1021); they are not implied by the older onboarding evidence above. diff --git a/docs/lark-connection.zh.md b/docs/lark-connection.zh.md index 19d2d5ce64..68f8bc2e21 100644 --- a/docs/lark-connection.zh.md +++ b/docs/lark-connection.zh.md @@ -2,21 +2,19 @@ 先按 [图文安装教程](/zh/docs/installation) 安装插件,确认本地 Bot DM 正常后,再继续本页。 -把一个 PersonaBot 接入工作群:先连接应用机器人,再绑定 Bot 身份并授权群,最后选择消息进入 Bot 收件箱还是本地频道。 +三步把一个 PersonaBot 接入 Lark:连接应用机器人、把它绑定到 Bot、发一条测试消息。绑定后,私聊这个应用、或在它所在的群里 @ 它,消息都会进入这个 Bot 的收件箱,Bot 在原会话回复。不需要保存投递目标,也不需要逐个授权会话。 -本页以 **Lark 国际版、应用机器人、测试群、只收 @** 为起点。飞书用户使用同样的配置顺序,但要选择飞书平台、使用飞书开放平台创建的应用。 +本页以 **Lark 国际版、应用机器人、测试群** 为起点。飞书用户使用同样的配置顺序,但要选择飞书平台、使用飞书开放平台创建的应用。 ## 先分清三个设置 -| 设置 | 在哪里 | 决定什么 | -| ----------- | ------------------------------------------------------ | ---------------------------------- | -| IM 应用连接 | 设置 → IM机器人 → 飞书 | 本机连接哪个 Lark / 飞书应用机器人 | -| 外部身份 | Bot 私聊 → Channel sidebar → 外部身份 | 这个 Bot 在外部以谁的身份发言 | -| 外部连接器 | Bot 私聊 → 外部连接器,或 Group Channel 的详细 Profile | 哪个外部群、哪些消息进入这个频道 | +| 设置 | 在哪里 | 决定什么 | +| ----------- | ---------------------------------------------------- | ---------------------------------------------------------------------- | +| IM 应用连接 | 设置 → IM机器人 → 飞书(总览:设置 → IM 应用) | 本机连接哪个 Lark / 飞书应用机器人 | +| 外部身份 | Bot 私聊 → Channel sidebar → 外部身份 → **绑定应用** | 把应用绑定到这个 Bot,并管理它的会话:静音、规则、屏蔽、同步到 Channel | +| 外部连接器 | Bot 私聊 → Channel sidebar → 外部连接器 | 高级:修改 Channel 同步,或为不能直接发往会话的应用保存发送目标 | -**绑定应用后,私聊和 @ 就能用。** 在外部身份中点击 **+ 绑定应用** 选择这个飞书应用后,私聊它、或在它所在的群里 @ 它,消息会直接进入这个 Bot 的收件箱,Bot 在原会话回复;不需要保存投递目标、授权会话或打开收件开关。详见[外部身份](/zh/docs/channel-sidebar/external-identities)。 - -下面第 3、4 节的保存目标与授权群仍然有效,用于收集群里没有 @ 的普通消息、把群接入本地频道,或让 Bot 主动向群发消息。添加外部连接器不会给其他 Bot 借用这个应用。 +Bot 能在哪里发言由平台决定:应用被拉进了哪些群、发布了哪些权限。一个应用只属于一个 Bot,绑定不会把它借给其他 Bot。 本页结合真实界面入口与已完成 Lark 连接的测试 Profile。已连接截图来自 #823 验证的产品产物(`0.0.0-test.823.6` / Provider `4.32.0-botharness.2`);测试群为「BotHarness IM QA #78」。空表单截图用于说明填写位置,不代表已完成连接。App Secret 不出现在媒体中。右键(手机长按)截图,可查看原图。 @@ -70,7 +68,7 @@ node scripts/dev-instance.mjs \ [下载视频](/guides/lark/lark-setup-walkthrough.mp4) · [中文字幕](/guides/lark/lark-setup.zh.vtt) · [English captions](/guides/lark/lark-setup.en.vtt) -观看顺序:0:00 应用凭据与权限 → 0:32 本机连接 → 0:48 事件订阅与发布 → 1:12 投递目标 → 1:20 身份与群授权 → 1:36 消息来源。下文保留可逐项执行的步骤。 +观看顺序:0:00 应用凭据与权限 → 0:32 本机连接 → 0:48 事件订阅与发布 → 1:12 投递目标 → 1:20 身份与群授权 → 1:36 消息来源。视频记录的是早期流程:绑定前先保存投递目标并授权群;现在绑定应用即可。下文保留可逐项执行的步骤。 ## 2. 在开放平台准备应用机器人 @@ -90,7 +88,7 @@ Lark 打开 [Lark 开发者后台](https://open.larksuite.com/app);飞书打 - `im:message.group_msg`:获取群组中所有消息;普通消息收件、群历史与话题跟进需要。这是敏感权限,按需要开通。 - `im:resource`:上传图片、文件等资源;需要发送附件时开通。 -权限是否可申请、是否需要审批,以你的组织后台为准。开通全群消息权限后,BotHarness 仍按明确授权的群和收件条件处理,不会自动把所有消息放进 Inbox。接口细节可参考 [接收消息事件](https://open.larksuite.com/document/server-docs/im-v1/message/events/receive) 和 [获取历史消息](https://open.larksuite.com/document/server-docs/im-v1/message/get-2)。 +权限是否可申请、是否需要审批,以你的组织后台为准。开通全群消息权限后,BotHarness 只接收 **规则** 允许普通消息的群里的普通消息;私聊和 @ 按绑定自动接收。接口细节可参考 [接收消息事件](https://open.larksuite.com/document/server-docs/im-v1/message/events/receive) 和 [获取历史消息](https://open.larksuite.com/document/server-docs/im-v1/message/get-2)。 ### 对照真实后台配置 @@ -141,94 +139,63 @@ _图 1:Lark 应用选择国际版。密钥只输入本机设置,不粘贴到 ![实际测试应用显示「运行正常」;这是已连接状态,不是空配置示意](/guides/lark/06-connected.webp) -_图 1b:真实账号的绿色「运行正常」 只证明传输在线;还要绑定 PersonaBot、授权群,并核对消息来源与回复。_ - -接着为该账号配置一个**投递目标**: - -1. 打开已连接机器人的设置,找到 **投递目标 → 新建目标**。 -2. 从 **从已聊过的会话选择** 中选测试群,检查显示名称和群 Chat ID;或手动填写平台提供的原生群 ID(`oc_…`),不要填群名称或消息 ID。 -3. 填一个调用别名,例如 `lark-test-group`,点击 **测试**,到 Lark 群确认测试消息,再点击 **保存目标**。 - -全新应用可能没有会话候选。#824 实测中,绑定 PersonaBot 前的预备 @消息没有立即填充列表,且进入了 Provider 自己的独立会话路径。建议手动填入已核实的原生群 Chat ID,再测试、保存;不知道 ID 时请向群管理员核实。独立会话的回复或发现目标的消息,都不是 BotHarness 的 canonical 收件证据。 - -![真实账号的已保存群投递目标](/guides/lark/16-delivery-target.webp) - -_账号设置 → 投递设置 → 投递目标。这里的测试、保存目标只确认发送目的地;下一节还需给 PersonaBot 绑定身份并授权。_ - -## 4. 绑定 PersonaBot 身份并授权群 - -在 **Bot 模式**打开目标 Bot 的 DM,使用右侧 **Channel sidebar**。在 **外部身份** 中点击 **连接 Lark / 飞书**(配置引导),选择应用平台、IM账号和保存的指定群目标。每个群独立核对;其他群的成功不能替代本群验收。步骤的「定位」会高亮侧栏中对应的行,必要时自动展开 **外部连接器**;完成状态由实际配置和关联收件/回复决定。 +_图 1b:真实账号的绿色「运行正常」 只证明传输在线;把应用绑定到 PersonaBot、核对消息来源与回复是另外的检查。_ -![身份与群授权的操作示意:先绑定身份,再授权具体群,最后可选添加外部连接器](/guides/lark/05-identity-routing-annotated.webp) +账号已可用于绑定。收消息和回复都不需要投递目标,它只用于[保存的发送目标](#高级保存的发送目标)。 -_操作示意:①绑定身份 → ②授权具体群 → ③可选接入频道。这是基于截图的 AI 辅助合成标注图,演示账号尚未连接;下方保留原始界面截图供对照。_ +## 4. 把应用绑定到 PersonaBot -1. 找到 **外部身份 → + 绑定应用**,在 **应用** 中选择刚连接的应用机器人,点击 **绑定应用**。看到「已就绪」后,私聊和 @ 已经能进入收件箱;需要普通消息、频道或主动发送时,再继续下面的授权步骤。 -2. 在 **外部连接器** 中点击 **授权外部会话**,在 **外部会话授权** 弹窗中选择该 IM账号和刚保存的**发送目标**。 -3. 点击 **绑定并授权此目标**。核对群名称、账号和本地收件位置。 -4. 初次验证保留 **仅此 Bot 的 Inbox**、**只收 @**。不用额外创建本地群,也不会占用 Bot DM 的消息历史。 +在 **Bot 模式** 打开目标 Bot 私聊,使用右侧 **Channel sidebar**。 -![PersonaBot Profile 的绑定身份弹窗,IM账号尚未配置](/guides/lark/02-identity.webp) +1. 在 **外部身份** 中点击 **+ 绑定应用**,在 **应用** 中选择已连接的应用并确认。一个 Bot 可以绑定多个应用,包括多个 Lark 应用;已绑定到其他 Bot 的应用不会出现在列表中。 +2. 身份行显示 **已就绪**:私聊这个应用、或在它所在的群里 @ 它的消息,现在会进入这个 Bot 的收件箱。 +3. 选择 **新会话** 的接收方式。**自动接收**(默认)在新的私聊或群第一条被接收的消息到达时加入它;**先问我** 会把它放在 **等待处理**,由你允许。每个应用每小时最多自动加入 20 个新会话、同时最多 500 个活跃会话,超出的会等你处理。 -_本节图片为旧版 Profile 布局;现在这些设置位于右侧侧栏的「外部身份」/「外部连接器」。_ +![已绑定的 Lark 应用及其会话列表:等待处理、活跃、已静音、已屏蔽](/guides/channel-sidebar/19c-app-conversations-zh.webp) -_图 2:外部身份属于 PersonaBot。列表为空时,先检查上一步的应用连接、Provider 版本和保存目标。Human 扫码登录不等于绑定 Bot 身份。_ +_每个被接收的私聊或群都会出现在应用的会话列表里。**静音** 保留会话,但不再因它唤醒 Bot;**规则** 设置群里接收哪些消息,例如没有 @ 的普通消息;**屏蔽** 会持久拒绝这个会话,直到点击 **再次允许**,屏蔽期间的消息不会补发。_ -![PersonaBot Profile 中外部身份与外部会话授权的准备状态](/guides/lark/03-authorize.webp) +外部身份中的 **连接 Lark / 飞书**(配置引导)跟踪同样的三步:连接应用、绑定到这个 Bot、发消息验证。**定位** 会高亮对应控件;只有真实配置和对应的回复才会把步骤标记为完成。 -_图 3:先绑定身份,再在「外部连接器 → 授权外部会话」中授权具体群。示例尚未连接账号,所以授权控件尚不可用。_ +![配置引导的三个步骤:连接应用、绑定、发消息验证](/guides/lark/28-setup-guide.zh.webp) -![已绑定真实 Lark 身份与测试群,保留 Inbox-only 与只收 @](/guides/lark/07-granted.webp) +## 5. 可选:把会话同步到本地频道 -_图 3b:真实测试群已绑定并授权,群接收已连接;普通消息收件仍未开启。截图中“继承全局默认”与“只收 @”是不同的配置字段。_ +Lark 会话默认进入 Bot 收件箱。从应用新建同步的方式正在重新设计:之后会在 **外部连接器** 里选择任意已连接应用的会话(无论是否绑定了 Bot),作为上下文流入 Channel。 -每个 Bot 可以绑定多个平台,每个平台绑定一个身份。同一个 Bot 在不同本地群中仍使用自己的外部身份。共享频道的其他 Bot 如需对外回复,也要绑定自己的身份并获得目标群授权。 +已有的同步继续有效,可在 **外部连接器** 中修改、暂停或移除。同步使用会话本身的权限,不会新增群访问权限,也不会把应用借给其他 Bot。一个频道可以接收多个来源,一个来源也可以进入多个频道。**同步决定收到什么;每个成员 Bot 的注意力 / 唤醒策略决定何时处理。** 外部消息会在气泡上方显示来源名称,点击可查看详情。普通本地回复不会自动广播到 Lark。 -## 5. 可选:把消息显示在本地频道 +首次测试通过后再调整: -只想在 Lark 聊天、让消息进入 Bot Inbox,可以跳过这一节。如果希望团队在本地共同看到外部消息,打开已有 Group Channel 的详细 Profile;先把负责接收的 Bot 加为成员。也可以在 Bot DM 的侧栏 **外部连接器 → + 添加外部连接器** 中,把来源接入这个 DM 的消息历史。 +- 在 **设置 → Bot 设置** 管理全局默认;群的 **规则** 可以继承或覆盖它们。修改只影响以后到达的消息,不会导入过去的历史。 +- 要接收没有 @ 的普通消息,先发布 `im:message.group_msg` 和事件订阅,在群里发一条不 @ 的消息并刷新。真实投递验证通过前,普通消息接收保持不可用。 +- 要接收话题里没有 @ 的回复,需要显式跟进该话题。 -在 Group Channel 详细 Profile 的 **外部连接器 → 添加外部连接器** 中: - -1. 选择**已授权的 Lark 群**。这里不创建外部账号或新增群授权;列表为空就返回上一节。 -2. 确认**投递目标**:共享群频道、DM 消息历史,或仅 Bot Inbox。可选项取决于当前 Channel。 -3. 填入容易辨认的**连接器名称**,例如「Lark 团队工作群」。 -4. 保留**继承全局默认**,或明确选择**仅收 @接收身份**;开启接收并保存。 - -![添加外部连接器弹窗,展示来源、投递目标、名称和接收条件](/guides/lark/04-connector.webp) - -_图片为旧版 Profile 布局;在 Bot 私聊中,这些设置现在位于右侧侧栏的「外部连接器」。_ - -_图 4:来源、投递位置和接收条件分别选择。演示账号尚未授权群,所以保存按钮不可用;真实配置完成后才可保存。_ - -一个频道可接入多个来源,同一外部来源也可接入多个目标。**连接器决定收什么,每个成员 Bot 的 Attention / 唤醒策略决定何时处理**。外部消息会以来源名称显示在消息气泡上方,点击来源打开详情;在本地普通回复不会自动广播回 Lark。 +## 6. 确认真的跑通 -首次跑通后再调整高级行为: +先做小测试:在测试群中 **@ 应用机器人**,发送「请在这里回复 LARK-OK」。不要同时测试多个 Bot。 -- 全局默认在 **设置 → Bot 设置**中管理;Profile 可继承或覆盖,只影响后续消息,不补收历史。 -- 要接收不带 @ 的普通消息,先发布相应应用权限和事件订阅,在已授权测试群发送一条无 @ 消息,再刷新验证结果。尚未验证真实普通消息投递时,全量收件选项不可用。 -- 在群的收件与唤醒设置中选择按条数 / 时间汇总或逐条唤醒。进入 Inbox 不代表必须立刻回复,Bot 可以判断是否参与。 -- Bot 要继续接收某个话题的无 @ 回复,需要显式跟进话题;回复一次不会自动跟进。平台必须实际把这些事件投递给应用。 -- 暂停连接器的 Switch 保留配置与历史;删除连接器不删除已收消息。解绑身份是独立操作,会影响该 Bot 依赖此身份的外部行为。 +1. 消息出现在 Bot 私聊右侧的 **Bot 收件箱**,来源群、原发送者和内容正确;这个群出现在应用会话列表的 **活跃** 中。 +2. 来源详情显示外部消息 ID、Source Event ID,以及存在时的话题信息。 +3. Lark 原会话收到这个 Bot 自己的应用发出的 `LARK-OK`。话题测试应在同一话题回复。 +4. 发一条不 @ 的普通消息,确认除非该群的 **规则** 允许普通消息,否则它不会进入这个 Bot。 -## 6. 确认真的跑通 +**Lark 的绿色或灰色已读圆圈不能说明 Bot 是否收到消息。** 以本地收件箱的来源记录和真实回复为准。 -先做一个小测试:在授权的 Lark 群 **@应用机器人**发送「请在这里回复 LARK-OK」,不要同时测试多个 Bot 或多条路由。 +![真实来源详情、原生消息 ID 与 Source Event ID](/guides/lark/08-source.webp) -成功应能逐项确认: +_在 Bot 私聊侧栏展开 Bot 收件箱 → 群;消息已处理时,再展开已处理 / 已忽略。点击消息打开详情,展开来源详情与消息详情。_ -1. Bot DM 右侧的 **Bot 收件箱**出现该消息,来源是正确的 Lark 群,带原始发送人与消息内容。 -2. 点开来源详情,能看到外部消息 ID、Source Event ID,以及存在时的话题信息。 -3. Lark 原会话收到这个 Bot 以自己的应用身份发出的 `LARK-OK`。若在话题内测试,回复仍在原话题。 -4. 默认只收 @ 时,再发送一条不带 @ 的普通消息,确认它不会作为新的普通收件触发该 Bot。已显式跟进的话题按话题策略处理。 +## 高级:保存的发送目标 -![真实 Lark 话题消息的来源详情,显示外部会话和接收身份](/guides/lark/08-source.webp) +Lark 可以直接列出并发往会话,所以 Lark 应用收消息、回复和主动发消息都不需要保存目标。保存的发送目标只作为后备,用于 Provider 无法列出或直接发往会话的应用: -_图 5:这是已收件的真实话题消息。Source Event ID 是 BotHarness 的来源定位符;外部 message / thread / root / parent ID 由 Lark 提供,不能用本地 Channel ID 替代。_ +1. 在已连接机器人的设置中打开 **投递目标 → 新建目标**,选择或填写会话,点击 **测试** 后 **保存目标**。 +2. 在 Bot 私聊侧栏打开 **外部连接器 → 保存发送目标(高级)**,选择应用和已保存的目标并确认。 -在 Bot DM 右侧展开 **Bot 收件箱 → 群名**;若消息已处理,再展开「已处理或忽略」。点击消息打开 Modal,然后展开「来源详情」和「消息详情」。发送人名字无法解析时会显示平台 ID。 +![真实已保存的群投递目标](/guides/lark/16-delivery-target.webp) -**不要以 Lark 消息旁的绿色 / 灰色已读圆圈判断 Bot 是否收件**。以本地 Inbox 来源记录和实际外部回复为准。 +_截图为早期流程,当时绑定前必须先保存目标。_ ## 7. 在 Lark 私聊申请管理员配对 @@ -303,16 +270,16 @@ _本节图片为旧版 Profile 布局;现在配对从右侧侧栏的「外部 ## 常见问题 -| 现象 | 优先检查 | -| ----------------------------- | ------------------------------------------------------------------------------------------------------------- | -| 应用找不到、连接失败 | Lark / 飞书平台选项、组织、App ID / Secret、Host 是否运行 | -| 有连接但没有 IM账号可选 | 是否使用兼容的固定 Provider、账号是否在线、目标是否已经测试并保存;刷新侧栏 | -| 群不在已授权列表 | Bot 是否已绑定身份;是否保存了投递目标并在该 Bot 的「外部连接器」中明确授权;共享频道是否包含该 Bot | -| @消息不进 Inbox | 机器人是否在群;应用身份权限、`im.message.receive_v1` 长连接订阅、版本发布与管理员审批;本地授权和收件 Switch | -| 普通消息或话题无 @ 回复不进来 | `im:message.group_msg`、真实事件投递验证、群的接收条件;话题是否被显式跟进 | -| 历史读取返回 230027 | 应用身份的群消息读取权限及发布是否生效;Human 登录权限不能代替它 | -| 能收到,但不会回复 | 模型是否可用、Inbox / 唤醒状态;该 Bot 是否有自己的启用身份和该目标的发送授权 | -| 本地 DM 没有消息 | 检查是否选了 Inbox-only;这是有效的独立投递路径,不是丢消息 | +| 现象 | 优先检查 | +| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| 应用找不到、连接失败 | Lark / 飞书平台选项、组织、App ID / Secret、Host 是否运行 | +| 绑定应用时找不到应用 | 是否使用兼容的固定 Provider、账号是否在线;已绑定到其他 Bot 的应用不会列出(见 设置 → IM 应用) | +| 群不在会话列表 | 应用是否在群里、第一条消息是否 @ 了它;会话是否在等待处理(先问我或每小时上限)或已被屏蔽 | +| @消息不进 Inbox | 机器人是否在群;应用身份权限、`im.message.receive_v1` 长连接订阅、版本发布与管理员审批;身份是否启用、会话是否被静音或屏蔽 | +| 普通消息或话题无 @ 回复不进来 | `im:message.group_msg`、真实事件投递验证、群的接收条件;话题是否被显式跟进 | +| 历史读取返回 230027 | 应用身份的群消息读取权限及发布是否生效;Human 登录权限不能代替它 | +| 能收到,但不会回复 | 模型是否可用、Inbox / 唤醒状态;该 Bot 的身份是否启用、应用是否有 `im:message:send_as_bot` | +| 本地 DM 没有消息 | 会话默认只进入 Bot 收件箱;只有已有的同步(见 **外部连接器**)会显示到 Channel | 如果需要协助,提供平台、操作步骤、可公开的错误码和已经完成的检查。不要附 App Secret、访问令牌或群中无关的消息内容。 @@ -338,7 +305,7 @@ _测试消息到达指定群后,保存投递目标。_ ![账号目标身份与授权已确认](/guides/lark/23-new-app-guide.webp) -_前四步来自真实配置;点击引导不会推进完成状态。_ +_早期的五步引导;现在的引导只有三步(连接应用、绑定、验证)。步骤来自真实配置;点击引导不会推进完成状态。_ ![原生话题中的提及和新应用自己的回复](/guides/lark/24-new-app-topic-reply.webp) @@ -382,8 +349,8 @@ Computer/Browser 的首次授权覆盖原生会话,因此通知不提供审 在支持图片的 #1021 候选版本中,已授权的 Lark 图片或受支持的图文消息会显示在原 Channel 气泡内。上方的来源名称仍可打开来源详情。图片进入可见区域时加载;点击图片可放大,加载失败后可点击「重试」。文字和多张图片按原生顺序保留在一条消息中。 -需要将外部连接器的接收目的地设为消息历史。仅 Bot Inbox 的接收不会把图片放入 Channel。应用继续使用既有消息权限:群聊仅 @ 接收时,受支持的原生图文消息仍需真实 @Bot;此功能不会开启普通群消息权限。 +需要先把会话同步到 Channel。仅 Bot Inbox 的接收不会把图片放入 Channel。应用继续使用既有消息权限:群聊仅 @ 接收时,受支持的原生图文消息仍需真实 @Bot;此功能不会开启普通群消息权限。 -暂停外部连接器后,已取得的图片仍可查看,但该路径不再取得新图片。解绑身份或撤销来源授权后,该路径连缓存图片也不可访问;另一个独立有效的来源路径保持独立。刷新和重启保留已授权、已取得的图片;清除或缺失的原件不会重新下载。预览支持不超过 25 MiB 的 PNG、JPEG、GIF 和 WebP;超限或不支持的格式会明确提示。 +暂停同步后,已取得的图片仍可查看,但该路径不再取得新图片。解绑身份或撤销来源授权后,该路径连缓存图片也不可访问;另一个独立有效的来源路径保持独立。刷新和重启保留已授权、已取得的图片;清除或缺失的原件不会重新下载。预览支持不超过 25 MiB 的 PNG、JPEG、GIF 和 WebP;超限或不支持的格式会明确提示。 Human 查看图片不会让 Bot 获得图片理解能力,也不改变其注意力、模型上下文或权限。本候选版本的真实平台验收和截图记录在 [#1021](https://github.com/BotHarness/BotHarness/issues/1021),上方旧的新手引导证据不能代替本次验收。 diff --git a/docs/slack-connection.md b/docs/slack-connection.md index b8bc9236c3..044972485a 100644 --- a/docs/slack-connection.md +++ b/docs/slack-connection.md @@ -1,16 +1,16 @@ # Connect a Bot to Slack -Connect a PersonaBot to a **public Slack channel**: configure an application, connect it locally, bind the Bot's identity and authorize the channel. Start with messages that @mention the application bot, then verify a reply in the same Slack thread. +Connect a PersonaBot to Slack: configure an application, connect it locally and bind it to the Bot. After binding, @mentions of the app in channels it belongs to reach this Bot's Inbox, and the Bot replies in the same Slack thread. You don't authorize each channel. ## Understand the three settings -| Setting | Location | Purpose | -| ------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------- | -| IM application connection | Settings → IM bots → Slack | Connect this Host to a Slack application bot | -| External identity | Bot DM → Channel sidebar → External identities | Choose the identity this Bot uses to speak externally | -| External connector | Bot DM → External connectors, or a Group Channel's detailed Profile | Choose which external messages enter this local Channel or Bot Inbox | +| Setting | Location | Purpose | +| ------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| IM application connection | Settings → IM bots → Slack (overview: Settings → IM apps) | Connect this Host to a Slack application bot | +| External identity | Bot DM → Channel sidebar → External identities → **Bind app** | Bind the app to this Bot, then manage its conversations: mute, rules, block, sync to a Channel | +| External connector | Bot DM → Channel sidebar → External connectors | Advanced: edit Channel syncs and saved send targets | -Connecting an app does not authorize every Slack channel. Binding an identity does not mirror messages into a local DM. Each Bot replies with its own bound, authorized identity. +Where the Bot can speak is decided by Slack: the channels the app was invited to and the scopes you installed. Binding an app does not mirror messages into a local DM, and an app belongs to one Bot. ## 1. Prepare your environment @@ -86,27 +86,21 @@ _Permissions allow access; subscriptions determine which live events Slack sends _The connected account is the actual #868 installed-product result, not a filled-form mock. See Slack's [installation documentation](https://docs.slack.dev/authentication/installing-with-oauth/)._ -## 4. Bind the PersonaBot and authorize a channel +## 4. Bind the app to the PersonaBot -Open the PersonaBot's DM and, in the **Channel sidebar** on the right, expand **External identities** (外部身份). Select **+ Bind app** (绑定应用), choose the connected Slack app and click **Bind app**. Once the dialog shows **Ready**, DMs to the app and @mentions of it in channels it belongs to go straight to this Bot's Inbox, and the Bot replies in the same thread. The authorization steps below are only needed for unmentioned channel messages, a local Channel or proactive posts. See [External identities](/docs/channel-sidebar/external-identities). +Open the PersonaBot's DM and, in the **Channel sidebar** on the right, expand **External identities**. Select **+ Bind app**, choose the connected Slack app and confirm. Once the row shows **Ready**, @mentions of the app in channels it belongs to go straight to this Bot's Inbox, and the Bot replies in the same thread. See [External identities](/docs/channel-sidebar/external-identities). ![Bind identity dialog selecting an authenticated IM account](/guides/slack/08-bind.webp) -_Screenshots in this section show the earlier Profile layout; these settings now live in the sidebar's External identities / External connectors entries._ +_Screenshots in this section show the earlier Profile layout; binding now lives in the sidebar's External identities entry._ ![Actual bound Slack identity with status, enabled switch, edit and unbind actions](/guides/slack/09-identity.webp) -Next expand **External connectors** (外部连接器) and click **Authorize a conversation** (授权外部会话). In the **Conversation authorization** dialog, choose the bound account and the external public channel from the selectors, and authorize it. If it is missing, first check app installation and channel membership, then refresh. - -![Actual authorized Slack channel and its topic policy controls](/guides/slack/13-authorized.webp) - -_The retained QA account is bound to this explicitly authorized channel. Topic following is managed separately._ - -Use the connector rows under **External connectors** to manage that source's delivery route. Keep the initial condition **mentions only** and the destination **Bot Inbox only**. Inspect the enabled switch and runtime status; they are distinct from the identity's enabled switch. +Each channel appears in the app's conversation list after its first admitted message. **New conversations** decides whether it is admitted automatically (the default) or waits for you under **Waiting**. Use **Mute**, **Rules** and **Block** on a row to quiet, tune or refuse that channel; **Allow again** lifts a block without backfilling missed messages. ## 5. Verify a mention and same-thread reply -In the authorized Slack channel, send an actual @mention selected from Slack's mention picker, for example: +In a Slack channel the app belongs to, send an actual @mention selected from Slack's mention picker, for example: > @YourAppName Please reply “SLACK-OK” in this Slack thread using bridge_reply. Do not send a local DM. @@ -122,40 +116,37 @@ _The model replied BH868-PRODUCT-OK and BH868-RESTART-OK in the original Slack t Also send one plain, unmentioned message. With mentions-only intake and no explicit topic-follow policy, it should not enter this Bot's Inbox. Replying once does not automatically make the Bot follow every message in the thread. -## 6. Choose where messages go +## 6. Optional: sync a channel into a local Channel -| Destination | Result | -| ---------------------------------- | ---------------------------------------------------------------------------------------------------- | -| Bot Inbox only | External sources enter the Bot's Inbox without occupying local DM history | -| Explicit local DM or Group Channel | External sources appear in that Channel's history; each member Bot has its own Attention/wake policy | - -For a shared Channel, add the participating Bots, open its detailed Profile and choose **Add connector** (添加外部连接器). In a Bot DM, use the sidebar's **External connectors → + Add connector** instead. Select an already authorized Slack source, give the connector a recognizable name, choose its intake condition and save. The dialog identifies the local destination. It does not create an app or grant new external access. +By default Slack messages reach only the Bot Inbox and do not occupy local DM history. Adding a new sync is being redesigned: **External connectors** will take a conversation from any connected app and stream it into a Channel. Existing syncs keep working. Each member Bot of a shared Channel keeps its own Attention/wake policy. ![Actual shared Channel external connector dialog using an already authorized Slack group](/guides/slack/12-connector.webp) -_This #845 capture demonstrates shared-Channel configuration. Local discussions stay local; a Bot must explicitly choose an external source and use its own authorized identity to reply._ +_This #845 capture shows the earlier connector dialog. Edit, pause or remove existing syncs under **External connectors**._ -The external connector's switch pauses new intake while keeping its configuration and history. Deleting a connector does not delete earlier messages. Disabling an external identity and disabling a connector are separate actions. +Pausing a sync stops new intake while keeping its configuration and history. Removing it does not delete earlier messages. Disabling an external identity and pausing a sync are separate actions. ### Ordinary messages, harvest and topic following -Start with @mentions. To collect ordinary text, first enable `channels:history` and `message.channels`, install the changed permissions, then send a plain test message in the authorized channel and refresh. BotHarness requires observed ordinary-message delivery before offering all-message intake or topic following. +Start with @mentions. To collect ordinary text, first enable `channels:history` and `message.channels`, install the changed permissions, then send a plain test message in the channel and refresh. BotHarness requires observed ordinary-message delivery before offering all-message intake or topic following. + +Open **Rules** on the channel's row and choose custom **all messages** intake only for the intended channel. Select count/time harvest (for example, 5 messages or 30 seconds), or safely queued immediate wake. For shared Channels, each member Bot chooses when to process those messages through its own Attention settings. Intake, waking and whether to reply are separate decisions. -In the group intake settings of **External connectors → Authorize a conversation**, choose custom **all messages** intake only for the intended source. Select count/time harvest (for example, 5 messages or 30 seconds), or safely queued immediate wake. For shared Channels, each member Bot chooses when to process those messages through its own Attention settings. Intake, waking and whether to reply are separate decisions. +A Bot may explicitly follow or leave one native Slack topic. Human can inspect or override this in **Topic following**. Unmentioned replies in a followed topic can enter; unrelated topics keep their own rules. A Bot can also request bounded channel, nearby or thread context with continuation, without importing the history as new Inbox messages. -A Bot may explicitly follow or leave one native Slack topic within its authorization. Human can inspect or override this in **Topic following** (话题跟进). Unmentioned replies in a followed topic can enter; unrelated topics keep their own rules. A Bot can also request bounded channel, nearby or thread context with continuation, without importing the history as new Inbox messages. +**Bot settings → Slack defaults** controls the default intake, harvest and identity-enabled behavior. A channel's **Rules** can inherit these values or explicitly override them. Global changes apply to subsequent events for inheriting configurations; they do not create accounts, admit channels or replay old history. -**Bot settings → Slack defaults** controls the default intake, harvest and identity-enabled behavior. Profiles can inherit these values or explicitly override them. Global changes apply to subsequent events for inheriting configurations; they do not create accounts, authorize channels or replay old history. +Slack can address channels directly, so a saved send target is never required. The **External connectors → Save a send target (advanced)** fallback stays for apps that can't. ## Troubleshooting -| Symptom | Check and next step | -| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Verify/connect fails | Check the `xoxb-…` / `xapp-…` fields, App Token `connections:write`, workspace installation and Socket Mode. Use the same app's token pair. | -| Online, but no source appears | Invite the app to the public channel; verify `app_mention`, actual @mention, PersonaBot binding, channel authorization and enabled connector. | -| All-message option is unavailable | Check `message.channels` plus installed `channels:history`; send a new unmentioned text message and refresh to verify real delivery. | -| Context/file reading is denied | Inspect installed scopes and current membership. Reinstall after approved scope changes; file processing also needs an authorized Workspace. | -| Source handled, but no reply | Inspect source/Outbox state and native thread. The Bot may choose silence or have lost its own identity/Grant. An uncertain send is not blindly retried. | -| Reconnect or restart needed | Retain the same Profile; run only one receiver for this app. Resume and verify a new message. Reconnection does not backfill missed history. | +| Symptom | Check and next step | +| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Verify/connect fails | Check the `xoxb-…` / `xapp-…` fields, App Token `connections:write`, workspace installation and Socket Mode. Use the same app's token pair. | +| Online, but no source appears | Invite the app to the public channel; verify `app_mention`, an actual @mention, the app bound to this Bot, and the channel not waiting, muted or blocked. | +| All-message option is unavailable | Check `message.channels` plus installed `channels:history`; send a new unmentioned text message and refresh to verify real delivery. | +| Context/file reading is denied | Inspect installed scopes and current membership. Reinstall after approved scope changes; file processing also needs an authorized Workspace. | +| Source handled, but no reply | Inspect source/Outbox state and native thread. The Bot may choose silence or have lost its own identity/Grant. An uncertain send is not blindly retried. | +| Reconnect or restart needed | Retain the same Profile; run only one receiver for this app. Resume and verify a new message. Reconnection does not backfill missed history. | This guide covers qualified **public-channel** mentions, ordinary text, bounded context, native topic following, explicit shared-Channel routing, mentioned-file processing and explicit reports. Private channels/Slack DMs, unmentioned file-share intake, edits/deletions, workspace-wide search, automatic gap backfill and a scheduled morning-report service are outside this qualification. Slack read indicators are not BotHarness receipt evidence. For provider-specific boundaries and future adapters, see the [IM integration guide](/dev/guides/im-provider-integration/). diff --git a/docs/slack-connection.zh.md b/docs/slack-connection.zh.md index 9818a589af..713f2ac739 100644 --- a/docs/slack-connection.zh.md +++ b/docs/slack-connection.zh.md @@ -1,16 +1,16 @@ # 连接 Slack -将 PersonaBot 接入一个 **Slack 公共频道**:配置应用、本机连接、绑定 Bot 身份,再授权频道。先使用 @应用机器人 的消息,确认 Bot 能在原 Slack 话题中回复。 +将 PersonaBot 接入 Slack:配置应用、本机连接、把应用绑定到 Bot。绑定后,在应用所在的频道里 @ 它,消息会进入这个 Bot 的收件箱,Bot 在原 Slack 话题回复。不需要逐个授权频道。 ## 先区分三个设置 -| 设置 | 位置 | 作用 | -| ----------- | ------------------------------------------------------ | ----------------------------------------- | -| IM 应用连接 | 设置 → IM机器人 → Slack | 让本机连接一个 Slack 应用机器人 | -| 外部身份 | Bot 私聊 → Channel sidebar → 外部身份 | 决定这个 Bot 以谁的身份对外发言 | -| 外部连接器 | Bot 私聊 → 外部连接器,或 Group Channel 的详细 Profile | 决定哪些外部消息进入本地频道或 Bot 收件箱 | +| 设置 | 位置 | 作用 | +| ----------- | ---------------------------------------------------- | ---------------------------------------------------------------------- | +| IM 应用连接 | 设置 → IM机器人 → Slack(总览:设置 → IM 应用) | 让本机连接一个 Slack 应用机器人 | +| 外部身份 | Bot 私聊 → Channel sidebar → 外部身份 → **绑定应用** | 把应用绑定到这个 Bot,并管理它的会话:静音、规则、屏蔽、同步到 Channel | +| 外部连接器 | Bot 私聊 → Channel sidebar → 外部连接器 | 高级:修改 Channel 同步与保存的发送目标 | -绑定应用后,私聊和 @ 应用的消息直接进入 Bot 收件箱;不带 @ 的频道消息仍需授权频道。绑定应用不会自动把外部消息镜像到本地 DM。每个 Bot 对外回复都使用自己的绑定身份与授权。 +Bot 能在哪里发言由 Slack 决定:应用被邀请进了哪些频道、安装了哪些权限。绑定应用不会把外部消息镜像到本地 DM;一个应用只属于一个 Bot。 ## 1. 准备环境 @@ -86,23 +86,17 @@ _权限决定允许访问什么;事件订阅决定 Slack 实时推送什么。 _这是 #868 实际安装产品的连接结果,不是填满表单的模拟图。参见 Slack 的[安装说明](https://docs.slack.dev/authentication/installing-with-oauth/)。_ -## 4. 绑定 PersonaBot 并授权频道 +## 4. 把应用绑定到 PersonaBot -打开 PersonaBot 的私聊,在右侧 **Channel sidebar** 展开 **外部身份**,点击 **+ 绑定应用**,选择已连接的 Slack 应用,点击 **绑定应用**。弹窗显示「已就绪」后,私聊这个应用、或在它所在的频道里 @ 它,消息会直接进入这个 Bot 的收件箱,Bot 在同一话题回复。下面的授权步骤只在需要收集频道里不带 @ 的消息、接入本地频道或主动发消息时才需要。详见[外部身份](/zh/docs/channel-sidebar/external-identities)。 +打开 PersonaBot 的私聊,在右侧 **Channel sidebar** 展开 **外部身份**,点击 **+ 绑定应用**,选择已连接的 Slack 应用并确认。身份行显示「已就绪」后,在应用所在的频道里 @ 它,消息会直接进入这个 Bot 的收件箱,Bot 在同一话题回复。详见[外部身份](/zh/docs/channel-sidebar/external-identities)。 ![绑定身份对话框:选择已经认证的 IM 账号](/guides/slack/08-bind.webp) -_本节图片为旧版 Profile 布局;现在这些设置位于右侧侧栏的「外部身份」/「外部连接器」。_ +_本节图片为旧版 Profile 布局;现在绑定位于右侧侧栏的「外部身份」。_ ![真实 Slack 绑定身份:状态、启用开关、编辑与解绑](/guides/slack/09-identity.webp) -随后展开 **外部连接器**,点击 **授权外部会话**,在 **外部会话授权** 弹窗中从选择器选择绑定账号与外部公共频道,完成授权。如果找不到频道,先核对应用安装、频道成员资格,再刷新。 - -![真实已授权 Slack 频道与独立话题策略管理](/guides/slack/13-authorized.webp) - -_现有 QA 账号已绑定到此明确授权的频道。话题跟进单独管理。_ - -在 **外部连接器** 的连接器行中管理此来源的投递路径。初次测试保持 **仅收 @**,目标选择 **仅 Bot Inbox**。核对启用开关与运行状态;它们与身份的启用开关相互独立。 +每个频道在第一条被接收的消息到达后出现在应用的会话列表里。**新会话** 决定它自动接收(默认),还是先放在 **等待处理** 由你允许。在会话行上用 **静音**、**规则**、**屏蔽** 让频道安静、调整接收或拒绝它;**再次允许** 解除屏蔽,但不会补发错过的消息。 ## 5. 验证 @ 收件与原话题回复 @@ -122,37 +116,34 @@ _模型在原 Slack 话题回复了 BH868-PRODUCT-OK 与 BH868-RESTART-OK。验 再发一条不带 @ 的普通消息。仅收 @、且未显式跟进话题时,这条消息不应进入该 Bot 的 Inbox。回复一次不会自动跟进此话题的全部消息。 -## 6. 选择消息进入哪里 +## 6. 可选:把频道同步到本地频道 -| 目标 | 效果 | -| -------------------------- | --------------------------------------------------------------- | -| 仅 Bot Inbox | 外部来源进入 Bot 收件箱,不占用本地 DM 历史 | -| 显式本地 DM 或群组 Channel | 外部来源进入该频道历史;各成员 Bot 使用独立 Attention/唤醒策略 | - -要接入共享频道,先把参与 Bot 加入本地 Channel,打开详细 Profile,点击 **添加外部连接器**。在 Bot 私聊中,则使用侧栏的 **外部连接器 → + 添加外部连接器**。选择已经授权的 Slack 来源,设置易识别名称、接收条件后保存。弹窗会显示本地目标;此操作不会创建应用或扩大外部授权。 +Slack 消息默认只进入 Bot 收件箱,不占用本地 DM 历史。新建同步的方式正在重新设计:之后会在 **外部连接器** 里选择任意已连接应用的会话,流入 Channel。已有的同步继续有效。共享频道中的每个成员 Bot 保持各自的 Attention/唤醒策略。 ![真实共享频道外部连接器表单:复用已经授权的 Slack 群](/guides/slack/12-connector.webp) -_这张 #845 截图展示共享频道配置。本地讨论留在本地;Bot 对外回复时必须显式选择来源,并使用自己的授权身份。_ +_这张 #845 截图为早期的连接器表单。已有的同步可在 **外部连接器** 中修改、暂停或移除。_ -外部连接器开关暂停新收件,保留配置与历史。删除连接器不会删除旧消息。身份启停与连接器启停分别管理。 +暂停同步会停止新收件,保留配置与历史。移除同步不会删除旧消息。身份启停与同步启停分别管理。 ### 普通消息、汇总与话题跟进 -先从 @消息开始。要收普通文本,先开通 `channels:history` 与 `message.channels`、安装变更后的权限,再在授权频道发一条普通测试消息并刷新。BotHarness 需要观察到实际普通消息投递,才会提供全量收件或话题跟进。 +先从 @消息开始。要收普通文本,先开通 `channels:history` 与 `message.channels`、安装变更后的权限,再在频道发一条普通测试消息并刷新。BotHarness 需要观察到实际普通消息投递,才会提供全量收件或话题跟进。 + +在频道会话行打开 **规则**,只为需要的频道自定义 **全部消息**,选择数量/时间汇总(例如 5 条或 30 秒),或安全排队的逐条唤醒。共享频道中的每个 Bot,通过自己的 Attention 设置决定何时处理。收件、唤醒、是否回复分别判断。 -在 **外部连接器 → 授权外部会话** 的群收件设置中,只为需要的来源自定义 **全部消息**,选择数量/时间汇总(例如 5 条或 30 秒),或安全排队的逐条唤醒。共享频道中的每个 Bot,通过自己的 Attention 设置决定何时处理。收件、唤醒、是否回复分别判断。 +Bot 可以显式跟进或退出一个原生 Slack 话题;Human 可在 **话题跟进** 查看、覆盖。跟进话题中的无 @ 回复可以进入,其他话题沿用各自规则。Bot 也可按需读取有界频道历史、附近上下文或话题,通过续页继续读取;这些历史不会被当作新消息灌入 Inbox。 -Bot 可以在授权范围内显式跟进或退出一个原生 Slack 话题;Human 可在 **话题跟进** 查看、覆盖。跟进话题中的无 @ 回复可以进入,其他话题沿用各自规则。Bot 也可按需读取有界频道历史、附近上下文或话题,通过续页继续读取;这些历史不会被当作新消息灌入 Inbox。 +**Bot 设置 → Slack 默认设置** 管理默认收件、汇总和身份启用行为。频道的 **规则** 可继承或显式覆盖。全局修改作用于仍继承的配置及后续事件,不会创建账号、接收新频道或重放旧历史。 -**Bot 设置 → Slack 默认设置** 管理默认收件、汇总和身份启用行为。Profile 可继承或显式覆盖。全局修改作用于仍继承的配置及后续事件,不会创建账号、授权频道或重放旧历史。 +Slack 可以直接发往频道,所以永远不需要保存发送目标。**外部连接器 → 保存发送目标(高级)** 只作为不支持直接发往会话的应用的后备。 ## 排查问题 | 现象 | 检查与处理 | | ---------------------- | -------------------------------------------------------------------------------------------------------------------- | | 验证/连接失败 | 核对 `xoxb-…` 与 `xapp-…` 字段、App Token 的 `connections:write`、工作区安装与 Socket Mode;两种令牌应来自同一应用。 | -| 在线但没有来源 | 确认机器人已加入公共频道,订阅 `app_mention`,使用真正 @,并核对 Bot 绑定、频道授权与连接器启用。 | +| 在线但没有来源 | 确认机器人已加入公共频道,订阅 `app_mention`,使用真正 @;核对应用已绑定到这个 Bot,频道没有在等待处理、静音或屏蔽。 | | 无法选择全部消息 | 核对 `message.channels` 与已安装的 `channels:history`;发送新的普通文本后刷新,确认真实投递。 | | 上下文/文件读取被拒绝 | 检查已安装权限与当前成员资格;批准权限变更后重新安装。文件处理还需要授权 Workspace。 | | 来源已处理却没有回复 | 查看来源/Outbox 状态与原生话题。Bot 可能选择不回复,或自身身份/授权已失效;结果不确定的发送不会盲目重试。 | diff --git a/docs/wechat-connection.md b/docs/wechat-connection.md index 1b9bebda24..89fe863207 100644 --- a/docs/wechat-connection.md +++ b/docs/wechat-connection.md @@ -18,15 +18,15 @@ The entry capture precedes pairing and has no independently recorded exact Clien ## 2. Bind the PersonaBot identity -Open the PersonaBot DM and, in the **Channel sidebar** on the right, expand **External identities**. Click **+ Bind app** and bind the connected WeChat account. Its row shows the name, its status and an enable switch. Once the dialog shows **Ready**, DMs from the person who scanned this WeChat Bot go straight to this Bot's Inbox, and the Bot replies in the same DM; other contacts and WeChat groups stay out. The authorization in the next section is only needed for a local Channel or proactive reports. +Open the PersonaBot DM and, in the **Channel sidebar** on the right, expand **External identities**. Click **+ Bind app** and bind the connected WeChat account. Its row shows the name, its status and an enable switch. Once the row shows **Ready**, DMs from the person who scanned this WeChat Bot go straight to this Bot's Inbox, and the Bot replies in the same DM; other contacts and WeChat groups stay out. You don't authorize the conversation. ![Real paired WeChat identity in the PersonaBot Profile](/guides/wechat/identity-bound.jpg) -_Screenshot shows the earlier Profile layout; these settings now live in the sidebar's External identities / External connectors entries._ +_Screenshot shows the earlier Profile layout; binding now lives in the sidebar's External identities entry._ -## 3. Authorize the owner DM and enable intake +## 3. Manage the owner DM -Expand **External connectors** and click **Authorize a conversation**. In the **Conversation authorization** dialog, select the QR-paired owner conversation and explicitly authorize it. Enable **WeChat DM intake**. This first slice delivers only to the Bot Inbox; it does not insert external text into local Human DM history or expose group/mention/topic controls. Replies use the same Bot's bound identity and valid source continuation. +After the owner's first message, the DM appears under **Active** in the app's conversation list. Use **Mute** to stop it waking the Bot or **Block** to refuse it until **Allow again**. Messages reach only the Bot Inbox; they are not inserted into local Human DM history, and there are no group, mention or topic controls. Replies use the same Bot's bound identity and valid source continuation. ## 4. Check a real text and its reply @@ -154,7 +154,7 @@ The #908 live test received an item-ID-only quote: WeChat supplied neither the q The #909 installed-product test used `0.0.0-test.909.2` with the unchanged Provider `4.32.0-botharness.10`. One paired-owner conversation delivered the same canonical Source Event to a local shared Group and the receiving Bot's separate Inbox. The Human confirmed all four original-WeChat replies. -Keep the receiving PersonaBot's WeChat identity and paired-owner DM authorization. Create a local Group with this Bot and its collaborators. In its detailed Profile, choose **External connectors → Add connector**, select the authorized paired-owner conversation, name the connector and save. The fixed condition is **Paired-owner DM messages**: there are no mention or topic controls. A local Group is not a native WeChat group. +Keep the receiving PersonaBot's WeChat app bound. Create a local Group with this Bot and its collaborators. Adding a new sync is being redesigned around **External connectors**; an existing sync keeps working, and its name or pause state is edited there. The fixed condition is **Paired-owner DM messages**: there are no mention or topic controls. A local Group is not a native WeChat group. ![WeChat-specific connector configuration, light theme](/guides/wechat/channel-connector-config-light.jpg) @@ -186,13 +186,13 @@ This short sampled browser recording shows the real connector switch and saved s ## 12. Send an external-only text report -Keep the PersonaBot's own enabled WeChat identity and explicitly authorize its QR-paired owner DM. In its DM sidebar, open **External connectors → Authorize a conversation**. A qualified Provider exposes **Message → Send message**. Write a unique report and send explicitly. It creates a canonical Outbox report in WeChat only, without a local Human DM mirror or new Inbox Admission. The Bot can use the same capability through `bridge_targets`, `bridge_post` and `bridge_outbox`. This adds no scheduler. +Keep the PersonaBot's own enabled WeChat identity. Until the Provider lists WeChat conversations for proactive posts, reports use the advanced fallback: in the Bot DM sidebar, open **External connectors → Save a send target (advanced)** and save the QR-paired owner DM. A qualified Provider exposes **Message → Send message**. Write a unique report and send explicitly. It creates a canonical Outbox report in WeChat only, without a local Human DM mirror or new Inbox Admission. The Bot can use the same capability through `bridge_targets`, `bridge_post` and `bridge_outbox`. This adds no scheduler. ![Qualified owner-DM posting controls, light theme](/guides/wechat/proactive-after-light.jpg) ![The same real Profile in dark theme](/guides/wechat/proactive-after-dark.jpg) -_Screenshots show the earlier Profile layout; these settings now live in the sidebar's External connectors entry._ +_Screenshots show the earlier Profile layout; the saved send target now lives in the sidebar's External connectors → Save a send target (advanced)._ The paired owner first sends a message in the original WeChat conversation while this Bot's authorized intake is running. Private context stays inside the Provider and is never renewed by synthetic heartbeats. A local retention ceiling does not promise server validity. Missing context or native rejection fails with recovery instructions: check identity/authorization/intake, send a fresh message in the same DM, then explicitly request a new report. Re-pairing needs new authorization; another contact cannot substitute for this conversation. @@ -224,9 +224,9 @@ The recording shows real Profile input, send, Outbox settlement and receipt insp ## Pause or reconnect -Disable DM intake to stop future receipt while retaining configuration and history. Revoke the target authorization or unbind the identity to remove its authority. Re-pairing changes the identity fingerprint and requires explicit reauthorization; stale source continuations must not be reused. Restart with the same Profile to retain local pairing, canonical source records and Outbox outcomes. +Mute the DM to stop it waking the Bot, or Block it to refuse future messages, while retaining history. Unbind the app to remove its authority. Re-pairing changes the identity fingerprint and needs a new binding; stale source continuations must not be reused. Restart with the same Profile to retain local pairing, canonical source records and Outbox outcomes. -If text does not arrive, check the connected account, enabled identity and owner-DM authorization. Messages from other contacts and groups remain unsupported. This candidate supports owner text, files, qualified images and platform voice transcripts; section 8 describes the separately qualified original-audio candidate. Section 9 describes the separately qualified native-video candidate; its real native intake, processing and Human-confirmed original-DM video receipt are verified. If a reply is refused because its original continuation expired or is absent, send a new text in the paired conversation; the Bot must not borrow another conversation. An unknown send outcome must not be blindly resent. +If text does not arrive, check the connected account, the enabled identity and whether the owner DM is muted or blocked. Messages from other contacts and groups remain unsupported. This candidate supports owner text, files, qualified images and platform voice transcripts; section 8 describes the separately qualified original-audio candidate. Section 9 describes the separately qualified native-video candidate; its real native intake, processing and Human-confirmed original-DM video receipt are verified. If a reply is refused because its original continuation expired or is absent, send a new text in the paired conversation; the Bot must not borrow another conversation. An unknown send outcome must not be blindly resent. ## Verification and scope diff --git a/docs/wechat-connection.zh.md b/docs/wechat-connection.zh.md index bc565665e2..4f57cf338f 100644 --- a/docs/wechat-connection.zh.md +++ b/docs/wechat-connection.zh.md @@ -18,15 +18,15 @@ ## 2. 绑定 PersonaBot 的外部身份 -打开 PersonaBot 的 DM,在右侧 **Channel sidebar** 展开 **外部身份**,点击 **+ 绑定应用**,绑定已连接的微信账号;行内显示名称、状态及启用开关。弹窗显示「已就绪」后,扫码者本人私聊这个微信 Bot 的消息会直接进入这个 Bot 的收件箱,Bot 在原私聊回复;其他联系人和微信群的消息不会进入。下一节的授权只在需要接入本地频道或主动发送报告时才需要。 +打开 PersonaBot 的 DM,在右侧 **Channel sidebar** 展开 **外部身份**,点击 **+ 绑定应用**,绑定已连接的微信账号;行内显示名称、状态及启用开关。身份行显示「已就绪」后,扫码者本人私聊这个微信 Bot 的消息会直接进入这个 Bot 的收件箱,Bot 在原私聊回复;其他联系人和微信群的消息不会进入。不需要授权会话。 ![PersonaBot Profile 中真实配对的微信身份](/guides/wechat/identity-bound.jpg) -_图片为旧版 Profile 布局;现在这些设置位于右侧侧栏的「外部身份」/「外部连接器」。_ +_图片为旧版 Profile 布局;现在绑定位于右侧侧栏的「外部身份」。_ -## 3. 授权扫码者私聊并开启收件 +## 3. 管理扫码者私聊 -展开 **外部连接器**,点击 **授权外部会话**,在 **外部会话授权** 弹窗中选择扫码绑定者私聊并明确授权,再开启 **微信私聊收件**。此切片只投递到 Bot Inbox,不会把外部文字加入本地 Human DM 历史,也不会显示群收件、@ 或话题设置。对外回复使用当前 Bot 自己的绑定身份及有效的原消息续接能力。 +扫码者发出第一条消息后,这个私聊会出现在应用会话列表的 **活跃** 中。用 **静音** 让它不再唤醒 Bot,或用 **屏蔽** 拒绝它,直到点击 **再次允许**。消息只进入 Bot 收件箱,不会加入本地 Human DM 历史,也没有群收件、@ 或话题设置。对外回复使用当前 Bot 自己的绑定身份及有效的原消息续接能力。 ## 4. 核对真实文字与原私聊回复 @@ -154,7 +154,7 @@ Bot 可在已有可写工作区授权下,用 `bridge_attachment_save` 的 `rep #909 安装版验收使用产品 `0.0.0-test.909.2` 和未变更的 Provider `4.32.0-botharness.10`。同一个扫码者私聊将同一 canonical Source Event 投递到本地共享群聊和接收 Bot 独立的 Inbox。Human 已确认四次原微信私聊回复均收到。 -保留接收 PersonaBot 的微信身份及扫码者私聊授权。创建本地群聊,加入此 Bot 和需要看来源的协作 Bot。在群聊详细 Profile 选择**外部连接器 → 添加外部连接器**,选择已授权的扫码者私聊、填写名称并保存。接收条件固定为**扫码绑定者私聊消息**,不提供 @ 或话题控件。本地群聊不代表原生微信群。 +保持接收 PersonaBot 的微信应用处于绑定状态。创建本地群聊,加入此 Bot 和需要看来源的协作 Bot。新建同步的方式正在围绕 **外部连接器** 重新设计;已有的同步继续有效,可在那里修改名称或暂停。接收条件固定为**扫码绑定者私聊消息**,不提供 @ 或话题控件。本地群聊不代表原生微信群。 ![微信专属连接器配置,浅色主题](/guides/wechat/channel-connector-config-light.jpg) @@ -186,13 +186,13 @@ Bot 可在已有可写工作区授权下,用 `bridge_attachment_save` 的 `rep ## 12. 发送纯外部主动文字报告 -保留 PersonaBot 自己启用的微信身份,并显式授权其扫码者私聊。在 Bot 私聊侧栏打开**外部连接器 → 授权外部会话**,已资格验证的 Provider 会显示**消息 → 发送消息**。输入唯一报告并明确发送,报告进入 canonical Outbox,只投递微信,不插入本地 Human DM,也不产生新的 Inbox 收件。Bot 可通过 `bridge_targets`、`bridge_post` 和 `bridge_outbox` 使用同一能力;本片不新增定时系统。 +保留 PersonaBot 自己启用的微信身份。在 Provider 能列出微信会话用于主动发送之前,报告使用高级后备方式:在 Bot 私聊侧栏打开 **外部连接器 → 保存发送目标(高级)**,保存扫码者私聊;已资格验证的 Provider 会显示**消息 → 发送消息**。输入唯一报告并明确发送,报告进入 canonical Outbox,只投递微信,不插入本地 Human DM,也不产生新的 Inbox 收件。Bot 可通过 `bridge_targets`、`bridge_post` 和 `bridge_outbox` 使用同一能力;本片不新增定时系统。 ![已授权扫码者私聊的主动文字控件,浅色主题](/guides/wechat/proactive-after-light.jpg) ![同一真实 Profile,深色主题](/guides/wechat/proactive-after-dark.jpg) -_图片为旧版 Profile 布局;现在这些设置位于右侧侧栏的「外部连接器」。_ +_图片为旧版 Profile 布局;保存的发送目标现在位于右侧侧栏的「外部连接器 → 保存发送目标(高级)」。_ 扫码者需要先在原微信会话发消息,且该 Bot 的已授权接收正在运行。私有上下文留在 Provider 内部,不通过虚构心跳续期;本地保留上限不承诺服务器有效期。缺失上下文或原生拒绝会明确失败并提示恢复:检查身份、目标授权和接收,在同一个私聊发新消息,然后明确请求一条新报告。重新扫码需要重新授权,不能借用另一个联系人作为恢复捷径。 @@ -224,9 +224,9 @@ _图片为旧版 Profile 布局;现在这些设置位于右侧侧栏的「外 ## 暂停与重新连接 -关闭私聊收件会停止后续接收,并保留配置及历史。撤销目标授权或解绑身份会移除相应权限。重新扫码后身份指纹变化,需要明确重新授权;旧消息的续接能力不能跨身份复用。重启时沿用同一个 Profile,保留本地配对、canonical 来源与 Outbox 结果。 +静音私聊会让它不再唤醒 Bot,屏蔽会拒绝后续消息,历史都会保留。解绑应用会移除相应权限。重新扫码后身份指纹变化,需要重新绑定;旧消息的续接能力不能跨身份复用。重启时沿用同一个 Profile,保留本地配对、canonical 来源与 Outbox 结果。 -收不到文字时,检查账号连接、身份启用和扫码者私聊授权。其他联系人和群消息仍不支持。此候选支持扫码者文字、文件、已验证的图片和平台语音转写;第 8 节另行说明原始音频候选的资格验证。第 9 节说明另行资格验证的原生视频候选,已验证真实原生收件、处理,以及 Human 确认的原私聊视频送达。回复因原始续接能力缺失/过期被拒绝时,在绑定私聊发送新文字;Bot 不应借用另一个会话。发送结果不明时不能盲目重发。 +收不到文字时,检查账号连接、身份启用,以及扫码者私聊是否被静音或屏蔽。其他联系人和群消息仍不支持。此候选支持扫码者文字、文件、已验证的图片和平台语音转写;第 8 节另行说明原始音频候选的资格验证。第 9 节说明另行资格验证的原生视频候选,已验证真实原生收件、处理,以及 Human 确认的原私聊视频送达。回复因原始续接能力缺失/过期被拒绝时,在绑定私聊发送新文字;Bot 不应借用另一个会话。发送结果不明时不能盲目重发。 ## 验证与范围 diff --git a/packages/client/src/client/channel-bridge-list.tsx b/packages/client/src/client/channel-bridge-list.tsx index 446a6a41d4..814a8ef346 100644 --- a/packages/client/src/client/channel-bridge-list.tsx +++ b/packages/client/src/client/channel-bridge-list.tsx @@ -244,17 +244,9 @@ export function ChannelBridgeList({ } /> ))} - open('add')} - /> + {snapshot && !snapshot.bridges.length ? ( +
  • {t('bridge.empty')}
  • + ) : null} {children} ; rules(grantId: string, input: GroupReceptionInput): Promise; + channels?: { id: string; name: string }[]; }): ReactElement { const [open, setOpen] = useState(); const [confirm, setConfirm] = useState(); @@ -42,17 +44,33 @@ export function ExternalConversations({ const kind = (value: 'dm' | 'group') => t(value === 'dm' ? 'identity.kind.dm' : 'identity.kind.group'); const icon = (value: 'dm' | 'group') => (value === 'dm' ? 'user' : 'users'); - const small = (label: string, onClick: () => void, danger = false, pressed?: boolean) => ( - + const small = ( + label: string, + hint: string, + onClick: () => void, + danger = false, + pressed?: boolean, + ) => ( + + + + ); + const actions = (label: string, buttons: ReactNode, panel?: ReactNode): ReactNode => ( + <> +
    + {buttons} +
    + {panel} + ); const confirmBlock = (key: string, name: string, block: () => void): ReactNode => confirm === key ? ( @@ -81,10 +99,14 @@ export function ExternalConversations({ ) : undefined; + const synced = (grant: Grant) => + (grant.bridgeRoutes ?? []) + .map((route) => channels.find((c) => c.id === route.channelId)?.name) + .filter((name): name is string => name !== undefined); const entryRow = (grant: Grant) => { const scope = grant.receiveScope!; const name = grant.targetName || scope.conversationId; - const detail = + const panel = confirmBlock( grant.id, name, @@ -110,12 +132,17 @@ export function ExternalConversations({ ...(grant.lastMessageAt ? [t('identity.lastMessage', { time: time(grant.lastMessageAt) })] : []), + ...(synced(grant).length + ? [t('conversation.syncedTo', { names: synced(grant).join('、') })] + : []), ].join(' · ')} muted={grant.muted} - trailing={ + detail={actions( + name, <> {small( t(grant.muted ? 'conversation.unmute' : 'conversation.mute'), + t(grant.muted ? 'conversation.unmuteHint' : 'conversation.muteHint'), () => void change({ kind: 'mute', @@ -127,6 +154,7 @@ export function ExternalConversations({ {grant.groupPolicy ? small( t('conversation.rules'), + t('conversation.rulesHint'), () => { setConfirm(undefined); setOpen(open === grant.id ? undefined : grant.id); @@ -135,10 +163,15 @@ export function ExternalConversations({ open === grant.id, ) : null} - {small(t('conversation.block'), () => setConfirm(grant.id), true)} - - } - detail={detail} + {small( + t('conversation.block'), + t('conversation.blockHint'), + () => setConfirm(grant.id), + true, + )} + , + panel, + )} /> ); }; @@ -158,10 +191,12 @@ export function ExternalConversations({ kind(held.conversation.kind), t('conversation.seen', { count: held.count, time: time(held.lastSeenAt) }), ].join(' · ')} - trailing={ + detail={actions( + held.name, <> {small( t('conversation.allow'), + t('conversation.allowHint'), () => void change({ kind: 'allow', @@ -171,19 +206,24 @@ export function ExternalConversations({ expectedRevision: held.revision, }), )} - {small(t('conversation.block'), () => setConfirm(key), true)} - - } - detail={confirmBlock( - key, - held.name, - () => - void change({ - kind: 'block-held', - bindingId: identity.id, - conversation: held.conversation, - expectedRevision: held.revision, - }), + {small( + t('conversation.block'), + t('conversation.blockHint'), + () => setConfirm(key), + true, + )} + , + confirmBlock( + key, + held.name, + () => + void change({ + kind: 'block-held', + bindingId: identity.id, + conversation: held.conversation, + expectedRevision: held.revision, + }), + ), )} /> ); @@ -198,16 +238,20 @@ export function ExternalConversations({ t('conversation.blockedAt', { time: time(block.blockedAt) }), ].join(' · ')} muted - trailing={small( - t('conversation.allowAgain'), - () => - void change({ - kind: 'allow', - bindingId: identity.id, - conversation: block.conversation, - from: 'blocked', - expectedRevision: block.revision, - }), + detail={actions( + block.name || block.conversation.id, + small( + t('conversation.allowAgain'), + t('conversation.allowAgainHint'), + () => + void change({ + kind: 'allow', + bindingId: identity.id, + conversation: block.conversation, + from: 'blocked', + expectedRevision: block.revision, + }), + ), )} /> ); @@ -217,7 +261,9 @@ export function ExternalConversations({ {label} {rows.length} - {rows} + + {rows} + ) : null; if (!entries.length && !waiting.length && !blocked.length) diff --git a/packages/client/src/client/external-entries.tsx b/packages/client/src/client/external-entries.tsx index 0714a61620..16776bfcbc 100644 --- a/packages/client/src/client/external-entries.tsx +++ b/packages/client/src/client/external-entries.tsx @@ -23,7 +23,13 @@ function IdentitiesForBot({ t: ChannelSidebarEntryProps['t']; }): ReactElement { const { snapshot, failed, refresh, mount } = useMessagingSnapshot(slug, actions); - const { bots } = useClientState(); + const state = useClientState(); + const dm = state.channels.find((c) => c.type === 'dm' && c.botSlug === slug); + const bot = state.bots.find((b) => b.slug === slug); + const syncChannels = [ + ...(dm ? [{ id: dm.id, name: t('bridge.dmTarget', { name: bot?.displayName ?? slug }) }] : []), + ...(snapshot?.channelTargets ?? []), + ]; const [panel, setPanel] = useState<'pairing' | 'approval'>(); const [busy, setBusy] = useState(false); const [scopeFailed, setScopeFailed] = useState<'pairing' | 'approval'>(); @@ -69,14 +75,10 @@ function IdentitiesForBot({ await actions.messagingGroupPolicy(slug, grantId, input); await refresh(); }} - botName={(owner) => bots.find((bot) => bot.slug === owner)?.displayName ?? owner} - /> - state.bots.find((item) => item.slug === owner)?.displayName ?? owner} /> + slug, }: { snapshot: MessagingSnapshot | undefined; @@ -36,6 +37,7 @@ export function ExternalIdentityList({ mutate(input: MessagingIdentityInput): Promise; conversation(input: MessagingConversationInput): Promise; rules(grantId: string, input: GroupReceptionInput): Promise; + channels?: { id: string; name: string }[]; botName?(slug: string): string; }): ReactElement { const [mode, setMode] = useState<'bind' | 'bound' | 'edit' | 'reconnect' | 'unbind'>(); @@ -413,6 +415,7 @@ export function ExternalIdentityList({ t={t} change={(input) => operate(() => conversation(input))} rules={(grantId, input) => operate(() => rules(grantId, input))} + {...(channels ? { channels } : {})} /> ) : null} diff --git a/packages/client/src/client/lark-setup-guide.tsx b/packages/client/src/client/lark-setup-guide.tsx index c310b04903..7def81c8bd 100644 --- a/packages/client/src/client/lark-setup-guide.tsx +++ b/packages/client/src/client/lark-setup-guide.tsx @@ -4,59 +4,35 @@ import { Combobox } from './combobox.js'; import { revealSidebarAnchor } from './sidebar-anchor.js'; import { SidebarCardList, SidebarCardRow } from './sidebar-card.js'; import type { MessagingSnapshot } from '../../../core/src/messaging/outbound.js'; -import type { MessagingTarget } from '../../../core/src/messaging/provider.js'; import type { BotHarnessTranslate } from './locale.js'; import { Modal } from './modal.js'; import { useMountedResource } from './mounted-resource.js'; import { highlightLarkSetup } from './lark-setup-tour.js'; import { larkSetupState, LARK_SETUP_STEPS, type LarkSetupStep } from './lark-setup-state.js'; import { openImSettings } from './bot-settings-open.js'; +import { MessagingHelp } from './messaging-help.js'; export function LarkSetupGuide({ snapshot, t, refresh, - loadTargets, }: { snapshot: MessagingSnapshot | undefined; t: BotHarnessTranslate; refresh(): Promise; - loadTargets(providerId: string, accountRef: string): Promise; }): ReactElement { const [open, setOpen] = useState(false); const [platform, setPlatform] = useState<'lark' | 'feishu'>('lark'); const [accountKey, setAccountKey] = useState(''); - const [targetRef, setTargetRef] = useState(''); const [busy, setBusy] = useState(false); const [failed, setFailed] = useState(false); - const [targetRows, setTargetRows] = useState<{ - accountKey: string; - targets: MessagingTarget[]; - }>(); - const targetRequest = useRef(0); const stopTour = useRef<() => void>(); const root = useRef(null); const alive = useRef(false); const refreshing = useRef(false); const refreshRef = useRef(refresh); - const readTargets = async (key: string) => { - const sequence = ++targetRequest.current; - const account = snapshot?.accounts.find((a) => `${a.providerId}:${a.ref}` === key); - if (!account?.connected || account.platform !== 'feishu') { - setTargetRows(undefined); - return; - } - try { - const targets = await loadTargets(account.providerId, account.ref); - if (alive.current && sequence === targetRequest.current) - setTargetRows({ accountKey: key, targets }); - } catch (error) { - if (alive.current && sequence === targetRequest.current) throw error; - } - }; const refreshState = async () => { await refresh(); - await readTargets(accountKey); }; refreshRef.current = refreshState; const progressMount = useMountedResource(() => { @@ -82,20 +58,10 @@ export function LarkSetupGuide({ alive.current = true; return () => { alive.current = false; - ++targetRequest.current; stopTour.current?.(); }; }, []); - const targets = - targetRows?.accountKey === accountKey - ? targetRows.targets.filter((target) => target.receiveScope?.kind === 'group') - : []; - const selectedTarget = targets.find((target) => target.ref === targetRef); - const state = larkSetupState( - failed ? undefined : snapshot, - accountKey, - selectedTarget?.ref ?? '', - ); + const state = larkSetupState(failed ? undefined : snapshot, accountKey); const check = async () => { if (busy) return; setBusy(true); @@ -113,7 +79,7 @@ export function LarkSetupGuide({ setOpen(false); const doc = root.current?.ownerDocument; if (!doc) return; - if (step === 'account' || step === 'target') { + if (step === 'app') { const cancel = openImSettings( doc, (element) => { @@ -134,12 +100,11 @@ export function LarkSetupGuide({ const fail = () => { if (alive.current) setFailed(true); }; - const anchor = - step === 'identity' ? 'lark-bind' : step === 'grant' ? 'lark-grant' : 'lark-guide'; + const anchor = step === 'bind' ? 'lark-bind' : 'lark-guide'; let tour: (() => void) | undefined; const cancel = revealSidebarAnchor( doc, - step === 'grant' ? 'external-connectors' : 'external-identities', + 'external-identities', anchor, (element) => { tour = highlightLarkSetup( @@ -179,22 +144,49 @@ export function LarkSetupGuide({ className="bh-lark-setup-modal" >
    -

    {t('setup.summary')}

    -
    diff --git a/packages/client/src/client/lark-setup-state.ts b/packages/client/src/client/lark-setup-state.ts index 6369ae754f..b1fd334db5 100644 --- a/packages/client/src/client/lark-setup-state.ts +++ b/packages/client/src/client/lark-setup-state.ts @@ -1,13 +1,9 @@ import type { MessagingSnapshot } from '../../../core/src/messaging/outbound.js'; -export const LARK_SETUP_STEPS = ['account', 'target', 'identity', 'grant', 'verify'] as const; +export const LARK_SETUP_STEPS = ['app', 'bind', 'verify'] as const; export type LarkSetupStep = (typeof LARK_SETUP_STEPS)[number]; -export function larkSetupState( - snapshot: MessagingSnapshot | undefined, - accountKey: string, - targetRef = '', -) { +export function larkSetupState(snapshot: MessagingSnapshot | undefined, accountKey: string) { const account = snapshot?.accounts.find( (a) => a.platform === 'feishu' && `${a.providerId}:${a.ref}` === accountKey, ); @@ -24,8 +20,6 @@ export function larkSetupState( snapshot?.grants.filter( (g) => g.bindingId === identity?.id && - !!targetRef && - g.targetRef === targetRef && !g.revokedAt && g.availability === 'available' && g.receiveScope && @@ -43,13 +37,7 @@ export function larkSetupState( grants, receipt, providerReady: snapshot?.setup?.providerReady === true, - next: !account?.connected - ? 'account' - : !identity - ? 'identity' - : !grants.length - ? 'grant' - : 'verify', + next: !account?.connected ? 'app' : !identity ? 'bind' : 'verify', complete: snapshot?.setup?.providerReady === true && !!account?.connected && diff --git a/packages/client/src/client/lark-setup-styles.ts b/packages/client/src/client/lark-setup-styles.ts index 1a97ca0605..ac5f213caf 100644 --- a/packages/client/src/client/lark-setup-styles.ts +++ b/packages/client/src/client/lark-setup-styles.ts @@ -14,13 +14,25 @@ export const LARK_SETUP_CSS = ` font: inherit; } .bh-lark-setup-modal { width: min(640px, calc(100vw - 32px)); } -.bh-lark-setup-body { display: grid; gap: 12px; max-height: min(65vh, 580px); overflow-y: auto; min-width: 0; padding-right: 4px; } +.bh-lark-setup-body { display: grid; gap: 10px; max-height: min(65vh, 580px); overflow-y: auto; min-width: 0; padding-right: 4px; font-size: 13px; } .bh-lark-setup-body p { margin: 0; } .bh-lark-setup-body img { max-width: 100%; height: auto; } -.bh-lark-setup-body pre { white-space: pre-wrap; overflow-wrap: anywhere; padding: 10px; border: 1px solid var(--dsw-alias-border-l2); } -.bh-lark-setup-steps { padding-left: 20px; display: grid; gap: 12px; } -.bh-lark-setup-steps li > div { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; } -.bh-lark-setup-steps p { margin: 6px 0; color: var(--dsw-alias-label-secondary); } +.bh-lark-setup-body pre { margin: 6px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; font-size: 12px; } +.bh-lark-setup-intro { display: flex; align-items: center; gap: 4px; } +.bh-lark-setup-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; } +.bh-lark-setup-links { display: flex; flex-wrap: wrap; gap: 4px 16px; font-size: 12px; } +.bh-lark-setup-links a { color: var(--dsw-alias-state-business-primary); } +.bh-lark-setup-steps { margin: 0; padding: 0; list-style: none; border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; counter-reset: bh-step; } +.bh-lark-setup-steps li { padding: 6px 6px 6px 10px; counter-increment: bh-step; } +.bh-lark-setup-steps li + li { border-top: 1px solid var(--dsw-alias-border-l2); } +.bh-lark-setup-step { display: flex; align-items: center; gap: 8px; min-width: 0; } +.bh-lark-setup-step::before { content: counter(bh-step); display: inline-grid; place-items: center; flex: none; width: 20px; height: 20px; border-radius: 50%; border: 1px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-secondary); font-size: 11px; } +.bh-lark-setup-steps li[data-done="true"] .bh-lark-setup-step::before { content: "✓"; border-color: transparent; background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-base); } +.bh-lark-setup-step-title { display: flex; flex: 1; align-items: center; gap: 2px; min-width: 0; } +.bh-lark-setup-step-title strong { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.bh-lark-setup-steps li > pre, .bh-lark-setup-steps li > p { margin-left: 28px; } +.bh-lark-setup-steps li > p { margin-top: 6px; color: var(--dsw-alias-label-secondary); font-size: 12px; } +@media (max-width: 560px) { .bh-lark-setup-fields { grid-template-columns: minmax(0, 1fr); } } .bh-lark-tour :focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary); outline-offset: 2px; } @media (prefers-reduced-motion: reduce) { .driver-fade .driver-overlay, .driver-fade .driver-popover { animation: none; } } `; diff --git a/packages/client/src/client/locale.ts b/packages/client/src/client/locale.ts index 2df4dd0def..de0c68e343 100644 --- a/packages/client/src/client/locale.ts +++ b/packages/client/src/client/locale.ts @@ -79,8 +79,8 @@ export const zh = { '[BH-LARK-SETUP] 请在本话题使用 bridge_reply 只回复 LARK-SETUP-OK。不要另发群主线消息,也不要创建 Assignment。', 'setup.title': '连接 Lark / 飞书', 'setup.open': '配置引导', - 'setup.summary': - '沿用真实账号、身份和群授权。点击步骤只定位控件,不会发送消息,也不会把步骤标记为完成。', + 'setup.summary': '连接应用、绑定到 Bot,再发一条消息验证。', + 'setup.locateHint': '「定位」只会高亮对应控件,不会发送消息,也不会直接标记完成。', 'setup.platform': '应用平台', 'setup.lark': 'Lark(国际版)', 'setup.feishu': '飞书', @@ -99,32 +99,26 @@ export const zh = { 'setup.providerMissing': '尚未确认兼容的接收 Provider。请检查产品安装和 Provider 状态,再刷新。', 'setup.done': '已确认', 'setup.pending': '待确认', - 'setup.locate': '定位现有控件', + 'setup.locate': '定位', 'setup.verifyHint': - '在所选账号已授权群的话题中 @机器人发送以下测试文字。另发一条无 @消息,确认默认不收件。绿色/灰色已读圆圈不是收件证据。', + '在机器人所在群的话题中 @ 它,发送以下测试文字。不 @ 它的消息默认不会进入收件箱。外部平台的已读圆圈不能证明已收件。', 'setup.received': '已记录真实话题收件', 'setup.echo': '已观测到关联回复;请到 Lark 原话题核对。', 'setup.accepted': '平台已接受回复,尚未观测到回传;请到原话题核对。', 'setup.awaiting': '已收件,等待该身份回复。', 'setup.optional': - '初次保持仅 Bot Inbox、只收 @。完成后可在「外部连接器」中显式添加本地 DM/群投递;默认设置、Attention 和唤醒策略仍分别管理。', + '绑定后不需要授权会话。会话会自动出现在「外部身份」的会话列表里,可在那里静音、屏蔽或同步到 Channel。', 'setup.failed': '状态核对失败或控件不可用。保留现有配置,关闭引导后打开实际设置,再刷新重试。', 'setup.checking': '正在核对…', 'setup.refresh': '核对真实状态', 'setup.resume': '可以关闭后继续;重新打开会核对当前配置。断开、停用、解绑或撤销群授权会重新显示待确认步骤。', - 'setup.step.account': '连接应用账号', - 'setup.step.target': '测试并保存投递目标', - 'setup.step.identity': '绑定此 Bot 的外部身份', - 'setup.step.grant': '授权指定群并启用收件', - 'setup.step.verify': '验证 @话题收件和原话题回复', - 'setup.hint.account': '在「设置 → IM机器人 → 飞书」选择正确平台,使用应用凭据绑定并连接。', - 'setup.hint.target': '在账号的投递设置中选择测试群,确认原生 Chat ID;测试后保存目标。', - 'setup.hint.identity': '点击「绑定应用」,选择已连接的应用。一个应用只属于一个 Bot。', - 'setup.hint.grant': - '在侧栏「外部连接器」打开「授权外部会话」,选择已绑定账号和保存目标,再明确绑定并授权此目标。保持 Inbox-only 和只收 @。', - 'setup.hint.verify': - '在原生话题中发送指定 @测试;核对 Source Event 和本 Bot 自己身份的关联回复。', + 'setup.step.app': '在 IM 设置中连接应用', + 'setup.hint.app': '在「设置 → IM机器人 → 飞书」选择正确平台,填入应用凭据并连接。', + 'setup.step.bind': '把应用绑定到这个 Bot', + 'setup.hint.bind': '在「外部身份」点击「绑定应用」,选择刚连接的应用。一个应用只属于一个 Bot。', + 'setup.step.verify': '发消息验证收发', + 'setup.hint.verify': '把机器人拉进一个群,在话题中 @ 它发送下方测试文字,确认 Bot 在原话题回复。', 'defaults.restore': '恢复继承', 'defaults.threshold': '汇总阈值', @@ -826,10 +820,17 @@ export const zh = { 'identity.newConversations.ask': '先问我', 'identity.newConversationsHint': '自动接收:新的私聊或 @ 直接进入收件箱。先问我:新会话先出现在「等待处理」,只记录会话名称和次数,不保存消息内容;允许后从下一条消息开始接收。', + 'conversation.syncedTo': '已同步到 {names}', 'conversation.active': '活跃', 'conversation.muted': '已静音', 'conversation.waiting': '等待处理', 'conversation.blocked': '已屏蔽', + 'conversation.muteHint': '消息照常进入收件箱,但不唤醒 Bot。Bot 仍可回复。', + 'conversation.unmuteHint': '恢复:新消息会再次唤醒 Bot。', + 'conversation.rulesHint': '设置这个群里哪些消息会唤醒 Bot。', + 'conversation.blockHint': '丢弃这个会话的所有消息,并取消尚未发出的回复。需要确认。', + 'conversation.allowHint': '放行这个会话,之后的消息会进入收件箱并唤醒 Bot。', + 'conversation.allowAgainHint': '从下一条消息开始重新接收,屏蔽期间的消息不会补收。', 'conversation.mute': '静音', 'conversation.unmute': '取消静音', 'conversation.rules': '规则', @@ -883,10 +884,11 @@ export const zh = { 'identity.state.unavailable': '暂不可用', 'identity.state.rebind-required': '需要重新绑定', 'im.infoFor': '{title}的说明', - 'im.title': '外部会话授权', - 'im.summary': '管理已有外部来源、群收件和发送目标', - 'im.authorizeRow': '授权外部会话', - 'im.authorizeRowHint': '让 Bot 向一个外部会话发送消息', + 'im.title': '保存的发送目标(高级)', + 'im.summary': + '仅在应用不能列出或直接发往会话时使用:保存一个目标,让 Bot 主动发消息。收消息和回复不需要它。', + 'im.authorizeRow': '保存发送目标(高级)', + 'im.authorizeRowHint': '仅用于不支持直接发往会话的应用', 'im.back': '返回', 'im.refresh': '刷新', 'im.loading': '加载中…', @@ -2182,8 +2184,9 @@ export const en = { '[BH-LARK-SETUP] Use bridge_reply to reply only LARK-SETUP-OK in this topic. Do not send to the group mainline or create an Assignment.', 'setup.title': 'Connect Lark / Feishu', 'setup.open': 'Setup guide', - 'setup.summary': - 'Use real accounts, identities and group grants. Steps locate existing controls; opening the guide never sends or marks an operation successful.', + 'setup.summary': 'Connect the app, bind it to this Bot, then send one message to verify.', + 'setup.locateHint': + 'Locate only highlights the control; it never sends anything or marks the step done.', 'setup.platform': 'Application platform', 'setup.lark': 'Lark (international)', 'setup.feishu': 'Feishu', @@ -2204,36 +2207,30 @@ export const en = { 'A compatible receive Provider is not confirmed. Check the product installation and Provider, then refresh.', 'setup.done': 'Confirmed', 'setup.pending': 'Not confirmed', - 'setup.locate': 'Locate existing controls', + 'setup.locate': 'Locate', 'setup.verifyHint': - 'In a topic of the selected account’s authorized group, @mention the Bot with the test text below. Send an unmentioned message separately to check default rejection. External read circles do not prove receipt.', + 'In a topic of a group the Bot is in, @mention it with the test text below. Messages that don’t @mention it stay out of the Inbox by default. Read circles on the platform don’t prove receipt.', 'setup.received': 'Real topic receipt recorded', 'setup.echo': 'Correlated reply observed; check the original Lark topic.', 'setup.accepted': 'Platform accepted the reply; echo not observed. Check the original topic.', 'setup.awaiting': 'Received; waiting for this identity to reply.', 'setup.optional': - 'Start with Bot Inbox only and mentions only. Afterwards, explicitly add DM or Group routing in External connectors. Defaults, Attention and wake policy remain separate.', + 'No conversation authorization is needed after binding. Conversations show up in the External identities conversation list, where you can mute, block or sync them to a Channel.', 'setup.failed': 'Status check failed or controls are unavailable. Keep the configuration, open actual settings after closing the guide, then retry.', 'setup.checking': 'Checking…', 'setup.refresh': 'Check current state', 'setup.resume': 'Close and resume anytime. Reopening checks current configuration; disconnection, disable, unbind or grant revocation returns the relevant step to unconfirmed.', - 'setup.step.account': 'Connect application account', - 'setup.step.target': 'Test and save a delivery target', - 'setup.step.identity': 'Bind this Bot’s external identity', - 'setup.step.grant': 'Authorize the exact group and enable intake', - 'setup.step.verify': 'Verify topic @receipt and original-topic reply', - 'setup.hint.account': - 'In Settings → IM Bots → Feishu, choose the correct platform and connect with application credentials.', - 'setup.hint.target': - 'In account delivery settings, choose the test group and check its native Chat ID; test and save the target.', - 'setup.hint.identity': - 'Click Bind app and choose the connected app. An app belongs to one Bot only.', - 'setup.hint.grant': - 'In the sidebar’s External connectors, open Authorize a conversation, choose the bound account and saved target, then explicitly authorize. Keep Inbox-only and mentions-only.', + 'setup.step.app': 'Connect the app in IM settings', + 'setup.hint.app': + 'In Settings → IM Bots → Feishu, choose the correct platform, enter the app credentials and connect.', + 'setup.step.bind': 'Bind the app to this Bot', + 'setup.hint.bind': + 'Under External identities, click Bind app and choose the app you just connected. An app belongs to one Bot only.', + 'setup.step.verify': 'Send a test message', 'setup.hint.verify': - 'Send the designated @test in a native topic; inspect its Source Event and the correlated reply under this Bot’s own identity.', + 'Add the Bot to a group, @mention it in a topic with the test text below, and check that the Bot replies in the same topic.', 'defaults.restore': 'Restore inheritance', 'defaults.threshold': 'Harvest threshold', @@ -2970,10 +2967,21 @@ My relationship with the Human, established lore and topics to avoid: [fill in]. 'identity.newConversations.ask': 'Ask me first', 'identity.newConversationsHint': 'Admit automatically: new DMs and @mentions go straight to the Inbox. Ask me first: a new conversation waits under Waiting with only its name and message count, never the message text; once allowed, the next message is admitted.', + 'conversation.syncedTo': 'Synced to {names}', 'conversation.active': 'Active', 'conversation.muted': 'Muted', 'conversation.waiting': 'Waiting', 'conversation.blocked': 'Blocked', + 'conversation.muteHint': + 'Messages still reach the Inbox but don’t wake the Bot. The Bot can still reply.', + 'conversation.unmuteHint': 'New messages wake the Bot again.', + 'conversation.rulesHint': 'Choose which messages in this group wake the Bot.', + 'conversation.blockHint': + 'Drop every message from this conversation and cancel unsent replies. Asks to confirm.', + 'conversation.allowHint': + 'Let this conversation in: its messages reach the Inbox and wake the Bot.', + 'conversation.allowAgainHint': + 'Receive again from the next message. Messages sent while blocked are not recovered.', 'conversation.mute': 'Mute', 'conversation.unmute': 'Unmute', 'conversation.rules': 'Rules', @@ -3031,10 +3039,11 @@ My relationship with the Human, established lore and topics to avoid: [fill in]. 'identity.state.unavailable': 'Unavailable', 'identity.state.rebind-required': 'Rebind required', 'im.infoFor': 'About {title}', - 'im.title': 'Conversation authorization', - 'im.summary': 'Manage existing sources, group reception and send targets', - 'im.authorizeRow': 'Authorize a conversation', - 'im.authorizeRowHint': 'Let the Bot post to an external conversation', + 'im.title': 'Saved send target (advanced)', + 'im.summary': + 'Only for apps that can’t list or address conversations: save one target the Bot can post to proactively. Receiving and replying don’t need it.', + 'im.authorizeRow': 'Save a send target (advanced)', + 'im.authorizeRowHint': 'Only for apps that can’t post to conversations directly', 'im.back': 'Back', 'im.refresh': 'Refresh', 'im.loading': 'Loading…', diff --git a/packages/client/src/client/styles.ts b/packages/client/src/client/styles.ts index 08937866a5..390e0b53ad 100644 --- a/packages/client/src/client/styles.ts +++ b/packages/client/src/client/styles.ts @@ -2559,6 +2559,13 @@ html[data-botharness-motion='full'] .bh-skeleton-line { .bh-conversation-group-title { font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary); } .bh-conversation-group-title small { font-weight: 400; margin-left: 2px; } .bh-conversation-reason { font-size: 12px; color: var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary)); } +.bh-conversation-list .bh-card-main { padding-block: 7px 2px; } +.bh-conversation-list .bh-card-body { gap: 2px; } +.bh-conversation-list .bh-card-detail { display: grid; gap: 6px; padding-bottom: 6px; } +.bh-conversation-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; } +.bh-conversation-sync { display: grid; gap: 6px; } +.bh-conversation-sync-title { display: flex; align-items: center; gap: 2px; font-size: 12px; font-weight: 500; } +.bh-conversation-sync .bh-modal-footer { gap: 6px; } .bh-conversation-confirm { display: grid; gap: 8px; } .bh-conversation-confirm p { margin: 0; } .bh-modal-footer { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; width: 100%; justify-content: flex-end; } @@ -2567,7 +2574,7 @@ html[data-botharness-motion='full'] .bh-skeleton-line { .bh-sidebar-modal { width: min(440px, calc(100vw - 32px)); } .bh-sidebar-modal-form, .bh-im-grant-body { display: flex; flex-direction: column; gap: 12px; min-width: 0; font-size: 13px; } .bh-sidebar-modal-form p, .bh-im-grant-body p { margin: 0; } -.bh-sidebar-modal-form ul { margin: 0; padding-left: 18px; } +.bh-sidebar-modal-form ul:not(.bh-card-list) { margin: 0; padding-left: 18px; } .bh-sidebar-modal-subject { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-weight: 500; overflow-wrap: anywhere; } .bh-sidebar-modal-subject .bh-bridge-secondary { width: 100%; margin-top: 0; font-weight: 400; } .bh-muted { color: var(--dsw-alias-label-secondary); } diff --git a/packages/client/test/external-conversations.test.ts b/packages/client/test/external-conversations.test.ts index 6c22203764..33fa4955a0 100644 --- a/packages/client/test/external-conversations.test.ts +++ b/packages/client/test/external-conversations.test.ts @@ -136,3 +136,31 @@ it('groups conversations, confirms Block before sending it, and sends revision-c await act(async () => root.unmount()); container.remove(); }); + +it('shows where an entry is synced and offers no Sync action on the row', async () => { + Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }); + const container = document.createElement('div'); + document.body.append(container); + const root = createRoot(container); + const routed = { + ...entry('g1', 'Team', false), + bridgeRoutes: [{ channelId: 'c1' }], + } as unknown as MessagingSnapshot['grants'][number]; + await act(async () => + root.render( + createElement(ExternalConversations, { + identity, + snapshot: { ...snapshot, grants: [routed] }, + busy: false, + t: zhTranslate, + change: vi.fn(), + rules: vi.fn(), + channels: [{ id: 'c1', name: 'Intake' }], + }), + ), + ); + expect(container.textContent).toContain('已同步到 Intake'); + expect([...container.querySelectorAll('button')].map((b) => b.textContent)).not.toContain('同步'); + await act(async () => root.unmount()); + container.remove(); +}); diff --git a/packages/client/test/external-entries.test.ts b/packages/client/test/external-entries.test.ts index 8d96885e86..4b949865fe 100644 --- a/packages/client/test/external-entries.test.ts +++ b/packages/client/test/external-entries.test.ts @@ -172,12 +172,11 @@ it('lists connectors as rows with the conversation authorization row last', asyn const rows = [...host.querySelectorAll('.bh-card-row')]; expect(rows.map((row) => row.querySelector('.bh-card-title')?.textContent)).toEqual([ 'QA intake', - zhTranslate('bridge.add'), zhTranslate('im.authorizeRow'), ]); expect(rows[0]?.textContent).toContain('QA group'); expect(rows[0]?.textContent).toContain(zhTranslate('bridge.state.receiving')); - expect(rows[2]?.getAttribute('data-anchor')).toBe('lark-grant'); + expect(rows[1]?.getAttribute('data-anchor')).toBe('lark-grant'); await act(async () => rows[0]!.querySelector('.bh-card-main')!.click()); expect(host.querySelector('[role="dialog"]')?.getAttribute('aria-label')).toBe( zhTranslate('bridge.edit'), diff --git a/packages/client/test/lark-setup-guide.test.ts b/packages/client/test/lark-setup-guide.test.ts index 43f38dc075..5f9542ab5f 100644 --- a/packages/client/test/lark-setup-guide.test.ts +++ b/packages/client/test/lark-setup-guide.test.ts @@ -3,7 +3,6 @@ import { act, createElement, type PropsWithChildren, type MouseEventHandler } fr import { createRoot, type Root } from 'react-dom/client'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import type { MessagingSnapshot } from '../../core/src/messaging/outbound.js'; -import type { MessagingTarget } from '../../core/src/messaging/provider.js'; vi.mock('@deepseek-ai/dsh-client-ui-primitives', async () => ({ ...(await import('./primitive-mocks.js')).comboboxPrimitives(), @@ -14,6 +13,8 @@ vi.mock('@deepseek-ai/dsh-client-ui-primitives', async () => ({ }: PropsWithChildren<{ onClick?: MouseEventHandler; disabled?: boolean }>) => createElement('button', { onClick, disabled }, children), Tag: ({ children }: PropsWithChildren) => createElement('span', null, children), + Tooltip: ({ children }: PropsWithChildren) => children, + IconInfoOutlineRegular: () => null, Modal: ({ open, children, title }: PropsWithChildren<{ open: boolean; title: string }>) => open ? createElement('div', { role: 'dialog', 'aria-label': title }, children) : null, })); @@ -53,60 +54,46 @@ const snapshot = (): MessagingSnapshot => ({ setup: { providerReady: true, receipts: [] }, }); -const target: MessagingTarget = { - ref: 'qa', - name: 'QA', - digest: 'saved', - receiveScope: { kind: 'group', conversationId: 'oc-qa' }, -}; - async function choose(key: string) { await chooseOption(zhTranslate('im.account'), key); } -it('recognizes a saved group target independently and returns it to unconfirmed on disconnection', async () => { +const steps = () => + [...container.querySelectorAll('.bh-lark-setup-steps li')].map((li) => + li.textContent?.includes('已确认'), + ); + +it('walks three steps from a connected app to a bound Bot and returns to unconfirmed on disconnection', async () => { const current = snapshot(); - const loadTargets = vi.fn(async () => [target]); const refresh = vi.fn(async () => {}); const render = () => - root.render( - createElement(LarkSetupGuide, { snapshot: current, t: zhTranslate, refresh, loadTargets }), - ); + root.render(createElement(LarkSetupGuide, { snapshot: current, t: zhTranslate, refresh })); await act(async () => render()); await act(async () => container.querySelector('.bh-card-main')!.click()); + expect(steps()).toEqual([false, false, false]); await choose('dsh-im/feishu:one'); - expect(loadTargets).toHaveBeenCalledWith('dsh-im/feishu', 'one'); - expect(container.querySelectorAll('.bh-lark-setup-steps li')[1]?.textContent).toContain('待确认'); - await chooseOption(zhTranslate('im.target'), 'qa'); - expect(container.querySelectorAll('.bh-lark-setup-steps li')[1]?.textContent).toContain('已确认'); - expect(container.querySelectorAll('.bh-lark-setup-steps li')[2]?.textContent).toContain('待确认'); - expect(container.querySelectorAll('.bh-lark-setup-steps li')[4]?.textContent).toContain('待确认'); + expect(steps()).toEqual([true, false, false]); + current.identities = [ + { + id: 'identity', + botSlug: 'ada', + providerId: 'dsh-im/feishu', + accountRef: 'one', + platform: 'feishu', + name: 'one', + fingerprint: 'one', + enabled: true, + availability: 'available', + newConversations: 'auto', + revision: 1, + createdAt: 'now', + grantCount: 0, + scopes: [], + }, + ]; + await act(async () => render()); + expect(steps()).toEqual([true, true, false]); current.accounts[0]!.connected = false; await act(async () => render()); - expect(container.querySelectorAll('.bh-lark-setup-steps li')[1]?.textContent).toContain('待确认'); -}); - -it('ignores a delayed target response after a different account is selected', async () => { - let resolveOne: (value: MessagingTarget[]) => void = () => {}; - const delayed = new Promise((resolve) => { - resolveOne = resolve; - }); - const loadTargets = vi.fn(async (_provider: string, account: string) => - account === 'one' ? delayed : [], - ); - await act(async () => - root.render( - createElement(LarkSetupGuide, { - snapshot: snapshot(), - t: zhTranslate, - refresh: async () => {}, - loadTargets, - }), - ), - ); - await act(async () => container.querySelector('.bh-card-main')!.click()); - await choose('dsh-im/feishu:one'); - await choose('dsh-im/feishu:two'); - await act(async () => resolveOne([target])); - expect(container.querySelectorAll('.bh-lark-setup-steps li')[1]?.textContent).toContain('待确认'); + expect(steps()).toEqual([false, true, false]); }); diff --git a/packages/client/test/lark-setup-state.test.ts b/packages/client/test/lark-setup-state.test.ts index f47e3b0216..9636a6e63f 100644 --- a/packages/client/test/lark-setup-state.test.ts +++ b/packages/client/test/lark-setup-state.test.ts @@ -2,7 +2,7 @@ import { expect, it } from 'vitest'; import { larkSetupState } from '../src/client/lark-setup-state.js'; import type { MessagingSnapshot } from '../../core/src/messaging/outbound.js'; -it('does not trust a reply from another account, group or disabled identity', () => { +it('trusts only a reply in a receiving conversation of this Bot’s bound, enabled app', () => { const account = { providerId: 'lark', ref: 'one', @@ -63,31 +63,25 @@ it('does not trust a reply from another account, group or disabled identity', () intents: [], setup: { providerReady: true, receipts: [receipt] }, }; - expect(larkSetupState(snapshot, 'lark:one', 'group').complete).toBe(false); + expect(larkSetupState(snapshot, 'lark:one').complete).toBe(false); receipt.grantId = 'grant'; - expect(larkSetupState(snapshot, 'lark:one', 'group').complete).toBe(true); - snapshot.grants.push({ - ...grant, - id: 'grant-b', - targetRef: 'another-group', - receiveScope: { kind: 'group', conversationId: 'oc-b' }, - }); - expect(larkSetupState(snapshot, 'lark:one', 'another-group').grants).toHaveLength(1); - expect(larkSetupState(snapshot, 'lark:one', 'another-group').complete).toBe(false); + expect(larkSetupState(snapshot, 'lark:one').complete).toBe(true); + receipt.conversationId = 'oc-elsewhere'; expect(larkSetupState(snapshot, 'lark:one').complete).toBe(false); + receipt.conversationId = 'oc-qa'; receipt.echoObserved = false; - expect(larkSetupState(snapshot, 'lark:one', 'group').complete).toBe(true); + expect(larkSetupState(snapshot, 'lark:one').complete).toBe(true); snapshot.setup!.providerReady = false; - expect(larkSetupState(snapshot, 'lark:one', 'group').complete).toBe(false); + expect(larkSetupState(snapshot, 'lark:one').complete).toBe(false); snapshot.setup!.providerReady = true; identity.enabled = false; - expect(larkSetupState(snapshot, 'lark:one', 'group').next).toBe('identity'); + expect(larkSetupState(snapshot, 'lark:one').next).toBe('bind'); identity.enabled = true; account.connected = false; - expect(larkSetupState(snapshot, 'lark:one', 'group').complete).toBe(false); + expect(larkSetupState(snapshot, 'lark:one').next).toBe('app'); account.connected = true; grant.reception = 'off' as typeof grant.reception; - expect(larkSetupState(snapshot, 'lark:one', 'group').next).toBe('grant'); + expect(larkSetupState(snapshot, 'lark:one').complete).toBe(false); }); it('missing Provider, empty accounts and unknown state are never successful', () => { diff --git a/packages/core/src/messaging/inbound.ts b/packages/core/src/messaging/inbound.ts index dce462745e..55869a0af1 100644 --- a/packages/core/src/messaging/inbound.ts +++ b/packages/core/src/messaging/inbound.ts @@ -663,8 +663,11 @@ export function createInboundMessaging(options: { UNION ALL SELECT 1 FROM channel_placements WHERE source_event_id = ? LIMIT 1`) .get(candidate, candidate, candidate); + const implicitDefault = + latest.origin === 'implicit' && + (event.conversation.kind === 'dm' || event.mentionedAccount); const id = persistSource(db, latest, event); - if (existing) return id; + if (existing && !implicitDefault) return id; const row = db .prepare('SELECT payload_json FROM source_events WHERE source_event_id = ?') .get(id) as { payload_json: string }; @@ -702,6 +705,7 @@ export function createInboundMessaging(options: { continue; } } + if (implicitDefault) continue; const reason = event.conversation.kind === 'dm' ? 'human-dm' @@ -1496,7 +1500,7 @@ export function createInboundMessaging(options: { entry = providers.get(value.providerId); if (!entry?.provider.consume || !entry.provider.reply) throw new MessagingError('provider-incompatible'); - inspected = await bounded(entry.provider.inspect(value.accountRef, value.targetRef)); + inspected = await bounded(inspectGrant(entry.provider, value)); if ( providers.get(value.providerId) !== entry || !inspected.account.connected || diff --git a/packages/core/src/messaging/outbound.ts b/packages/core/src/messaging/outbound.ts index 8d4edbfc32..e8c539f3bc 100644 --- a/packages/core/src/messaging/outbound.ts +++ b/packages/core/src/messaging/outbound.ts @@ -1165,7 +1165,7 @@ export function createOutboundMessaging(options: { .filter( (route) => route.channelId === channelId || - (current.type === 'dm' && route.channelId === null), + (current.type === 'dm' && route.channelId === null && g.origin !== 'implicit'), ) .map((route) => ({ ...source(g), diff --git a/packages/core/test/messaging-default-traffic.test.ts b/packages/core/test/messaging-default-traffic.test.ts index f60c277f9a..438e9cc8b0 100644 --- a/packages/core/test/messaging-default-traffic.test.ts +++ b/packages/core/test/messaging-default-traffic.test.ts @@ -586,3 +586,31 @@ it('holds new conversations past the hourly limit instead of creating entries', expect.objectContaining({ conversation: { kind: 'dm', id: 'oc_20' }, reason: 'hourly-limit' }), ]); }); + +it('syncs an implicit group entry into a Channel, which then receives its later mentions', async () => { + const fx = await fixture(); + await fx.receive(mention('first')); + const entry = (await fx.core.externalMessaging.snapshot('ada')).grants[0]!; + const room = fx.core.channels.createGroup({ name: 'Team room', members: ['ada'] }); + await fx.core.externalMessaging.inbound.channelBridge(room.id, { + kind: 'add', + grantId: entry.id, + expectedGrantRevision: entry.revision, + delivery: 'channel', + name: 'Team', + enabled: true, + collection: 'mentions', + collectionInheritance: 'inherit', + }); + await fx.receive({ ...mention('second'), at: new Date(Date.now() + 1000).toISOString() }); + const placed = fx.query( + `SELECT json_extract(e.payload_json, '$.external.event.messageId') AS messageId FROM channel_placements p JOIN source_events e USING(source_event_id) WHERE p.channel_id = '${room.id}'`, + ); + expect(placed).toEqual([{ messageId: 'om-second' }]); + const routes = (await fx.core.externalMessaging.snapshot('ada')).grants[0]!.bridgeRoutes; + expect(routes?.map((route) => route.channelId)).toContain(room.id); + const synced = await fx.core.externalMessaging.channelBridges(room.id); + expect(synced.bridges.map((row) => [row.name, row.delivery])).toEqual([['Team', 'channel']]); + const dm = fx.core.channels.getOrCreateDm('ada', 'Ada')!; + expect((await fx.core.externalMessaging.channelBridges(dm.id)).bridges).toEqual([]); +});