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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ WeChat original voice can be prepared for playback, native quotes and retained l

### Changed

- **New conversations** is now a platform default too: Lark, Slack and Discord each have a **New conversations** choice in their global IM defaults (**Admit automatically** unless changed), and every bound app follows it until you pick a value for that app. Apps already set to **Ask me first** keep it ([#1134](https://github.com/BotHarness/BotHarness/issues/1134), [External identities guide](docs/channel-sidebar/external-identities.md)).
- 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)).
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@

### Changed

- **新会话** 也成了平台默认设置:飞书、Slack、Discord 的全局 IM 默认设置里各有一个 **新会话** 选项(不改就是 **自动接收**),每个已绑定应用都跟随它,除非你为这个应用单独选择。已经设为 **先问我** 的应用保持不变([#1134](https://github.com/BotHarness/BotHarness/issues/1134),[外部身份教程](docs/channel-sidebar/external-identities.md))。
- 「外部连接器」不再提供 **添加外部连接器** 和 **授权外部会话**(同步正在围绕“接入任意应用的会话”重新设计),只列出已有的同步,并保留 **保存发送目标(高级)** 给不能直接发往会话的应用。会话行会显示已同步到哪里,操作按钮(**静音**、**规则**、**屏蔽**)单独一行并各带说明提示。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))。
Expand Down
5 changes: 5 additions & 0 deletions docs/assets/pr/1134-new-conversation-default/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# #1134 New conversations as a platform default

Settings → Bot settings → External platform defaults, Lark tab: the new **New conversations** column (Admit automatically by default). Saving "Ask me first" here was read back through the bridge as `newConversations: "ask"`, revision 1.

![Defaults page](defaults.png)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions docs/channel-sidebar/external-identities.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ The first DM or @mention that reaches the Inbox records its conversation. Click
- **Admit automatically** (default): the message goes straight to the Inbox.
- **Ask me first**: the conversation waits under **Waiting**. Only its name, first and last seen time and message count are kept, never the message text. After **Allow**, the next message is admitted.

Each app starts on **Follow platform default**, which uses the **New conversations** column in the platform's global IM defaults (Lark, Slack and Discord each have one; **Admit automatically** unless you change it). Pick **Admit automatically** or **Ask me first** in the app dialog to override it for that app only.

To keep a busy app from flooding the Inbox, at most 20 new conversations per app join automatically each hour, and an app holds at most 500 active conversations. Past either limit, new conversations wait under **Waiting** and the row says which limit was hit. **Waiting** keeps the 200 most recent conversations per app.

![The app’s conversation list](/guides/channel-sidebar/19c-app-conversations-en.webp)
Expand Down
2 changes: 2 additions & 0 deletions docs/channel-sidebar/external-identities.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ Slack 和 Discord 同样适用:私聊应用、或在它所在的频道里 @
- **自动接收**(默认):消息直接进入收件箱。
- **先问我**:会话先放在 **等待处理**,只记录名称、首次和最近出现时间以及消息次数,不保存消息内容;点 **允许** 后,从下一条消息开始接收。

每个应用默认 **跟随平台默认**,即该平台全局 IM 默认设置里的 **新会话** 一栏(飞书、Slack、Discord 各一份,不改就是 **自动接收**)。在应用弹窗里选 **自动接收** 或 **先问我**,只对这个应用生效。

为防止繁忙的应用刷屏,每个应用每小时最多自动加入 20 个新会话,活跃会话最多 500 个。超过任一上限,新会话进入 **等待处理**,并在行内说明是哪一个上限。**等待处理** 每个应用最多保留最近的 200 个会话。

![应用的会话列表](/guides/channel-sidebar/19c-app-conversations-zh.webp)
Expand Down
25 changes: 22 additions & 3 deletions packages/client/src/client/external-identity-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function ExternalIdentityList({
const [accountKey, setAccountKey] = useState('');
const [name, setName] = useState('');
const [inheritEnabled, setInheritEnabled] = useState(false);
const [newConversations, setNewConversations] = useState<'auto' | 'ask'>('auto');
const [newConversations, setNewConversations] = useState<'auto' | 'ask' | 'inherit'>('inherit');
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const identities = snapshot?.identities ?? [];
Expand All @@ -65,7 +65,9 @@ export function ExternalIdentityList({
setSelected(row);
setName(row?.name ?? '');
setInheritEnabled(row?.enabledInheritance === 'inherit');
setNewConversations(row?.newConversations ?? 'auto');
setNewConversations(
row?.newConversationsInheritance === 'custom' ? row.newConversations : 'inherit',
);
setAccountKey('');
};
const close = () => {
Expand Down Expand Up @@ -400,8 +402,25 @@ export function ExternalIdentityList({
toggleLabel={t('identity.newConversations')}
value={newConversations}
disabled={busy}
onSelect={(value) => setNewConversations(value === 'ask' ? 'ask' : 'auto')}
onSelect={(value) =>
setNewConversations(
value === 'ask' || value === 'auto' ? value : 'inherit',
)
}
options={[
{
value: 'inherit',
label:
selected.newConversationsInheritance === 'custom'
? t('identity.newConversations.inheritPlain')
: t('identity.newConversations.inherit', {
value: t(
selected.newConversations === 'ask'
? 'identity.newConversations.ask'
: 'identity.newConversations.auto',
),
}),
},
{ value: 'auto', label: t('identity.newConversations.auto') },
{ value: 'ask', label: t('identity.newConversations.ask') },
]}
Expand Down
4 changes: 4 additions & 0 deletions packages/client/src/client/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,8 @@ export const zh = {
'identity.newConversations': '新会话',
'identity.newConversations.auto': '自动接收',
'identity.newConversations.ask': '先问我',
'identity.newConversations.inherit': '跟随平台默认({value})',
'identity.newConversations.inheritPlain': '跟随平台默认',
'identity.newConversationsHint':
'自动接收:新的私聊或 @ 直接进入收件箱。先问我:新会话先出现在「等待处理」,只记录会话名称和次数,不保存消息内容;允许后从下一条消息开始接收。',
'conversation.syncedTo': '已同步到 {names}',
Expand Down Expand Up @@ -3064,6 +3066,8 @@ My relationship with the Human, established lore and topics to avoid: [fill in].
'identity.newConversations': 'New conversations',
'identity.newConversations.auto': 'Admit automatically',
'identity.newConversations.ask': 'Ask me first',
'identity.newConversations.inherit': 'Follow platform default ({value})',
'identity.newConversations.inheritPlain': 'Follow platform default',
'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}',
Expand Down
15 changes: 15 additions & 0 deletions packages/client/src/client/messaging-defaults-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ function PlatformDefaultsSettings({
count: draft.count,
intervalSeconds: draft.intervalSeconds,
identityEnabled: draft.identityEnabled,
newConversations: draft.newConversations ?? 'auto',
};
const value = await saveMessagingDefaults(call, input);
if (mounted.current) {
Expand Down Expand Up @@ -233,6 +234,7 @@ function PlatformDefaultsSettings({
<tr>
<th>{t('identity.platform')}</th>
<th>{t('defaults.identity')}</th>
<th>{t('identity.newConversations')}</th>
</tr>
</thead>
<tbody>
Expand All @@ -246,6 +248,19 @@ function PlatformDefaultsSettings({
onChange={(identityEnabled) => change({ identityEnabled })}
/>
</td>
<td>
<select
aria-label={t('identity.newConversations')}
value={draft.newConversations ?? 'auto'}
disabled={busy}
onChange={(e) =>
change({ newConversations: e.target.value === 'ask' ? 'ask' : 'auto' })
}
>
<option value="auto">{t('identity.newConversations.auto')}</option>
<option value="ask">{t('identity.newConversations.ask')}</option>
</select>
</td>
</tr>
</tbody>
</table>
Expand Down
2 changes: 1 addition & 1 deletion packages/client/test/external-identity-list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ it('failed Switch writes retain the committed preference; stale Modal edits reta
expectedRevision: 7,
name: 'Retained edit',
inheritEnabled: false,
newConversations: 'auto',
newConversations: 'inherit',
enabled: true,
});
} finally {
Expand Down
2 changes: 2 additions & 0 deletions packages/client/test/messaging-defaults-settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
class Events extends EventTarget {
constructor() {
super();
stream = this;

Check warning on line 42 in packages/client/test/messaging-defaults-settings.test.ts

View workflow job for this annotation

GitHub Actions / lint, format, typecheck, test, build, docs

typescript(no-this-alias)

packages/client/test/messaging-defaults-settings.test.ts:42:7: Unexpected aliasing of 'this' to local variable.
}
close() {}
}
Expand Down Expand Up @@ -80,6 +80,7 @@
input: {
platform: 'feishu',
expectedRevision: 1,
newConversations: 'auto',
collection: 'all',
wake: 'digest',
count: 5,
Expand Down Expand Up @@ -123,7 +124,7 @@
class Events extends EventTarget {
constructor() {
super();
stream = this;

Check warning on line 127 in packages/client/test/messaging-defaults-settings.test.ts

View workflow job for this annotation

GitHub Actions / lint, format, typecheck, test, build, docs

typescript(no-this-alias)

packages/client/test/messaging-defaults-settings.test.ts:127:9: Unexpected aliasing of 'this' to local variable.
}
close() {}
}
Expand Down Expand Up @@ -188,6 +189,7 @@
input: {
platform: targetPlatform,
expectedRevision: 1,
newConversations: 'auto',
collection: 'mentions',
wake: 'digest',
count: 3,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/bridge/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1132,7 +1132,7 @@ export function createBridgeMethods(deps: BridgeMethodsDeps): BridgeMethods {
enabled: z.boolean(),
inheritEnabled: z.boolean().optional(),
expectedDefaultRevision: z.number().int().min(0).optional(),
newConversations: z.enum(['auto', 'ask']).optional(),
newConversations: z.enum(['auto', 'ask', 'inherit']).optional(),
})
.strict(),
z
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/database/schema-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,19 @@ const APPROVAL_MESSAGING_MIGRATION: SchemaMigration = {
},
};

const NEW_CONVERSATION_DEFAULT_MIGRATION: SchemaMigration = {
generation: 65,
module: 'messaging',
description: 'Let a bound app inherit the platform default new-conversation mode',
migrate(database) {
database.exec(`
ALTER TABLE messaging_bindings ADD COLUMN new_conversations_inherited INTEGER NOT NULL DEFAULT 1
CHECK (new_conversations_inherited IN (0, 1));
UPDATE messaging_bindings SET new_conversations_inherited = 0 WHERE new_conversations = 'ask';
`);
},
};

const BOUND_APP_ADMISSION_MIGRATION: SchemaMigration = {
generation: 61,
module: 'messaging',
Expand Down Expand Up @@ -1767,4 +1780,5 @@ export const BOT_HARNESS_SCHEMA_PLAN = defineSchemaPlan([
CONVERSATION_LIST_MIGRATION,
SEVERAL_APPS_MIGRATION,
CONVERSATION_INGEST_MIGRATION,
NEW_CONVERSATION_DEFAULT_MIGRATION,
]);
3 changes: 3 additions & 0 deletions packages/core/src/messaging/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const messagingDefaultsInput = z
count: z.number().int().min(1).max(100),
intervalSeconds: z.number().int().min(1).max(86400),
identityEnabled: z.boolean(),
newConversations: z.enum(['auto', 'ask']).optional(),
})
.strict();
export type MessagingDefaultsInput = z.infer<typeof messagingDefaultsInput>;
Expand Down Expand Up @@ -46,6 +47,7 @@ export function messagingDefaults<Platform extends string = 'feishu'>(
count: 5,
intervalSeconds: 30,
identityEnabled: true,
newConversations: 'auto',
revision: 0,
changedAt: '',
};
Expand All @@ -60,6 +62,7 @@ export function commitMessagingDefaults(
const { expectedRevision: _expected, ...preferences } = parsed;
const value = {
...preferences,
newConversations: preferences.newConversations ?? prior.newConversations ?? 'auto',
revision: prior.revision + 1,
changedAt: new Date().toISOString(),
};
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/messaging/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface MessagingIdentity {
enabled: boolean;
enabledInheritance?: 'inherit' | 'custom';
newConversations: MessagingNewConversations;
newConversationsInheritance?: 'inherit' | 'custom';
defaultRevision?: number;
revision: number;
createdAt: string;
Expand All @@ -29,7 +30,7 @@ export type MessagingIdentityInput =
enabled: boolean;
inheritEnabled?: boolean | undefined;
expectedDefaultRevision?: number | undefined;
newConversations?: MessagingNewConversations | undefined;
newConversations?: MessagingNewConversations | 'inherit' | undefined;
}
| { kind: 'reconnect'; id: string; expectedRevision: number }
| { kind: 'unbind'; id: string; expectedRevision: number };
Expand All @@ -50,6 +51,7 @@ interface BindingRow {
enabled: number;
enabled_inherited: number;
new_conversations: MessagingNewConversations;
new_conversations_inherited: number;
revision: number;
created_at: string;
revoked_at: string | null;
Expand All @@ -72,7 +74,11 @@ export function readMessagingIdentity(db: DatabaseSync, id: string): MessagingId
? messagingDefaults(db, r.platform).identityEnabled
: r.enabled === 1,
enabledInheritance: r.enabled_inherited === 1 ? 'inherit' : 'custom',
newConversations: r.new_conversations,
newConversations:
r.new_conversations_inherited === 1
? (messagingDefaults(db, r.platform).newConversations ?? 'auto')
: r.new_conversations,
newConversationsInheritance: r.new_conversations_inherited === 1 ? 'inherit' : 'custom',
defaultRevision: messagingDefaults(db, r.platform).revision,
revision: r.revision,
createdAt: r.created_at,
Expand Down
21 changes: 17 additions & 4 deletions packages/core/src/messaging/outbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,7 @@ export function createOutboundMessaging(options: {
throw new MessagingError('identity-stale');
const at = now();
db.prepare(
'UPDATE messaging_bindings SET revision = revision + 1, enabled = ?, display_name = ?, revoked_at = ?, enabled_inherited = ?, new_conversations = ? WHERE id = ?',
'UPDATE messaging_bindings SET revision = revision + 1, enabled = ?, display_name = ?, revoked_at = ?, enabled_inherited = ?, new_conversations = ?, new_conversations_inherited = ? WHERE id = ?',
).run(
enabled ? 1 : 0,
input.kind === 'update' ? input.name.trim() : latest.name,
Expand All @@ -785,9 +785,22 @@ export function createOutboundMessaging(options: {
: latest.enabledInheritance === 'inherit'
? 1
: 0,
input.kind === 'update'
? (input.newConversations ?? latest.newConversations)
: latest.newConversations,
input.kind === 'update' &&
input.newConversations !== undefined &&
input.newConversations !== 'inherit'
? input.newConversations
: (
db
.prepare('SELECT new_conversations FROM messaging_bindings WHERE id = ?')
.get(value.id) as { new_conversations: string }
).new_conversations,
input.kind === 'update' && input.newConversations !== undefined
? input.newConversations === 'inherit'
? 1
: 0
: latest.newConversationsInheritance === 'custom'
? 0
: 1,
value.id,
);
if (input.kind === 'unbind') {
Expand Down
24 changes: 24 additions & 0 deletions packages/core/test/messaging-lark-dm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,30 @@ it('Lark exact private chat uses canonical Inbox without mentions and replies th
});
}
expect((await core.externalMessaging.snapshot('ada')).identities).toHaveLength(2);
const asking = (await core.externalMessaging.snapshot('ada')).identities!.find(
(value) => value.platform === 'discord',
)!;
expect(asking).toMatchObject({ newConversations: 'ask', newConversationsInheritance: 'custom' });
const following = await core.externalMessaging.identity('ada', {
kind: 'update',
id: asking.id,
expectedRevision: asking.revision,
name: asking.name,
enabled: true,
newConversations: 'inherit',
});
expect(following).toMatchObject({
newConversations: 'auto',
newConversationsInheritance: 'inherit',
});
await core.externalMessaging.identity('ada', {
kind: 'update',
id: following.id,
expectedRevision: following.revision,
name: following.name,
enabled: true,
newConversations: 'ask',
});
expect(await createDshImProvider(transport, 'slack')!.accounts()).toEqual([]);
expect(provider.post).toBeUndefined();
expect(provider.readFile).toBeUndefined();
Expand Down
Loading
Loading