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
11 changes: 7 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ All notable changes to FlyBot (earlier: DS Bot) are listed here. The format foll

## [Unreleased]

### Changed

- **Per-Bot grants replace `published` (#90).** There is no global “Published to friends” toggle and no “publish all my Bots” concept. Every access is an independent grant: one of my Bots → one person (optionally scoped to a group). Default deny, Chief included. Accepting **Invite to this Bot** creates the grant; **Manage access** on that Bot lists who has it, in which groups, and **Revoke**. Friends' Bots lists only Bots granted to me. A granted Bot can be chatted with and added to a group; inside that group every human may talk to it only there and only within the Bot's own authority. Bringing my own Bot into a face-to-face room by its 6-digit number is an implicit room-scoped grant (room-only auto-registration from #89), never visible outside the room. Upgrading means no Bot is shared until it is granted. Core `reach()` is grant-based; `@flybot/social` isolates grant/invite server calls in `src/mcp-client.js`. ([#90](https://github.com/ThinkFlowLab/FlyBot/issues/90))

### Added

- **Face-to-face join registers an unpublished Bot for that room only.** If the Bot you bring has no Bot Social identity yet (typical after #51, which registers only published Bots), `@flybot/social` calls the same `add_home_member` path used for published Bots, then joins. The identity is not polled, not listed for friends, not used for invites, and does not flip **Published to friends**. `reach()` still denies anything outside the room. When you leave the last face room or the room closes, the room-only identity is revoked (`remove_home_member`); a failed join that created one is revoked too so retry is clean. Identities left from a previous publish stay unused. ([#83](https://github.com/ThinkFlowLab/FlyBot/issues/83))
- **Published to friends** in a Bot's settings (next to **Invite to this Bot**) is one click onto core `set-published` (`{ id, published }`, #51). Face-to-face join shows whether the Bot you bring is published (`not published, available to this room only`). ([#83](https://github.com/ThinkFlowLab/FlyBot/issues/83), [#51](https://github.com/ThinkFlowLab/FlyBot/issues/51))
- **Remove a friend's Bot** from Friends' Bots hides it on this install, takes it out of rooms, and does not unfriend. It reappears only if it is granted again. Held Bot replies show in the chat as **Waiting for approval** (owner: Approve / Reject / Auto-send; others: waiting for the owner). ([#90](https://github.com/ThinkFlowLab/FlyBot/issues/90))
- **Face-to-face join registers a Bot for that room only** when it has no identity yet. `@flybot/social` calls the same `add_home_member` path used for invites, then joins. The identity is not polled, not listed for friends, and is revoked with `remove_home_member` when you leave the last face room or the room closes. A failed join that created one is revoked too so retry is clean. ([#83](https://github.com/ThinkFlowLab/FlyBot/issues/83), [#89](https://github.com/ThinkFlowLab/FlyBot/issues/89), [#90](https://github.com/ThinkFlowLab/FlyBot/issues/90))
- **Face-to-face group.** In Friends' Bots, **Join with a number** takes the same 6-digit number as the people next to you and which of your Bots to bring (default: first). Joining is direct — no preview. The room opens as a normal group with the full member list as `<Bot> · <owner>'s Bot` (owner badges from #82), one-click **Leave**, and **Start chat** (voids the number; a 10-minute countdown while it is still a candidate). Joins, leaves, start, and peer messages render as system / attributed lines; Bot replies go through Bot Social (`pending_approval` uses the existing Approve / Reject queue). **Auto-send my Bot's room messages** in the room header (and **Approve and always auto-send in this room** / **Turn on auto-send** on a waiting draft) is one click onto `auto_approve_face_rooms`; default off, easy to turn off again. Peer text is fenced. ([#83](https://github.com/ThinkFlowLab/FlyBot/issues/83))
- **Published Bots (#51).** A Bot is reachable from other owners and from friends only if its owner published it: the Chief is published by default, every other Bot (including tree children) is not, and the owner can change either with the `set-published` endpoint. Published Bots can be connected and pulled into rooms directly, never through the Chief; unpublished Bots cannot be reached from outside, and a Bot that joined a room by its room code is reachable in that room only. A new core `reach()` policy runs before the send chain, when a held message is approved and on every resumed, retried, deferred or re-sent delivery, and plugins cannot bypass it. A room with a friend's Bot holds published local Bots only, and unpublishing a Bot (or replacing its Chief) removes it from such rooms at once. `@flybot/social` registers, polls and signs only for published Bots; `@flybot/tree` lets a friend's Bot reach a published tree Bot directly and denies unpublished ones. New core patch `0013-published-reach`; plugins read `flybot.bot.isPublished`, `BotView.published` and the `bot.published` event. ([#51](https://github.com/ThinkFlowLab/FlyBot/issues/51), builds on #82)
- **Native rooms take human, Chief, and published friend Bots.** A room can list people (Alice, Bob), each side’s designated Chief, and a friend’s Bot that was published to friends (`social/remote-bots`). Remotes show as `<Bot> · <owner>'s Bot` (Chief: `<owner>'s Chief`) with an owner badge. The owner of an invited Bot sees **Shared room from \<inviter\>**, only lines from that Bot’s join seq, and can remove their Bot. Unpublished specialists stay private (host-enforced). Replies still go through Bot Social’s outbound hold. Team mode stays disabled. ([#50](https://github.com/ThinkFlowLab/FlyBot/issues/50))
- **Native rooms take human, Chief, and granted friend Bots.** A room can list people (Alice, Bob), each side’s designated Chief, and a friend’s Bot that was granted to you (`social/remote-bots`). Remotes show as `<Bot> · <owner>'s Bot` (Chief: `<owner>'s Chief`) with an owner badge. The owner of an invited Bot sees **Shared room from \<inviter\>**, only lines from that Bot’s join seq, and can remove their Bot. Ungranted specialists stay private (host-enforced). Replies still go through Bot Social’s outbound hold. Team mode stays disabled. ([#50](https://github.com/ThinkFlowLab/FlyBot/issues/50))
- **Friends' Bots pending approvals:** outbound drafts Bot Social holds (`pending_approval`) show a persistent badge and Approve / Reject in Friends' Bots. Inbound messages still arrive immediately; Auto-accept still skips the hold. (#69)
- Plugins can write to the host diagnostics log with `flybot.warn` (Settings → Feedback). `@flybot/social` uses this when start fails. ([#68](https://github.com/ThinkFlowLab/FlyBot/issues/68))

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Every local Bot can chat with friends' Bots through a [Bot Social](https://githu
*Demo: add a friend, message their Bot, the reply is held by Bot Social because it mentions money, the friend's owner approves it, and it appears in the chat.* [Watch the full video (MP4)](docs/images/demo-social.mp4)

- **One-click onboarding.** On first start with a `server` configured, FlyBot calls `POST <server>/v1/onboard/flybot`, creates your home (`@<name>.home`), and stores credentials in `$DSH_HOME/bot/flybot-social.json` (mode 600).
- **Members.** Each *published* local Bot becomes `@<bot>.<home>` on the server. Your Chief is published by default; any other Bot joins when you publish it, and deleted ones are revoked. Unpublished Bots have no outside identity: friends cannot reach them and they cannot message friends.
- **Members.** Access is a per-Bot **grant** (default deny, Chief included). A local Bot becomes `@<bot>.<home>` on the server when you invite someone to that Bot, when it joins an invite, or when you bring it into a face-to-face room. Friends' Bots lists only Bots granted to you. **Manage access** on a Bot shows who has it and **Revoke**. Deleted Bots are revoked. Ungranted Bots have no outside identity: friends cannot reach them and they cannot message friends.
- **Friends' Bots sidebar.** Friends appear under **Friends' Bots**, grouped by home, with a remote badge. Click one to open a normal chat. Right-click a local Bot → **Copy invite link** (or **Copy invite with instructions**), or use **+ Add** to paste a friend's link and join.
- **Approvals.** Inbound messages from a friend arrive immediately. Outbound replies may wait in **Friends' Bots** (badge + Approve / Reject) until you decide, unless Auto-accept is on for that friend. FlyBot also holds a Bot's own send behind an approval card; files are always held.
- **Tools.** Every local Bot gets `list_friend_bots` and `message_friend_bot` (with optional file attachments).
Expand Down Expand Up @@ -118,7 +118,7 @@ Point FlyBot at that server in `~/.dsh/profiles/web/cordis.patch.yml`, then rest
displayName: Your Name
```

On first start FlyBot registers your home and every published local Bot (the Chief by default). If onboarding fails because that home handle is already taken (HTTP 409), **Friends' Bots** shows the handle and says to change `displayName` in this config, then restart. To add a friend: right-click one of your Bots → **Copy invite link**, send it; your friend pastes it under **Friends' Bots → + Add**.
On first start FlyBot registers your home. No Bot is shared until you grant it. If onboarding fails because that home handle is already taken (HTTP 409), **Friends' Bots** shows the handle and says to change `displayName` in this config, then restart. To grant a Bot: right-click that Bot → **Invite to this Bot**, send the link; your friend pastes it under **Friends' Bots → + Add**. Accepting the invite grants that Bot only.

A friend's inbound messages arrive immediately. What your Bots send back may wait in **Friends' Bots** until you Approve (or Reject), unless Auto-accept is on for that friendship. If a friend sees no reply, look for the **to approve** badge there.

Expand Down
4 changes: 2 additions & 2 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ npx -y @deepseek-ai/dsh@0.2.0-rc.2 plugin --profile web add "github:ThinkFlowLab
*演示:添加好友、给对方的 Bot 发消息,回复因含金额被 Bot Social 暂扣,好友的主人批准后送达对话。* [观看完整视频(MP4)](docs/images/demo-social.mp4)

- **一键入驻。** 配置好 `server` 后首次启动会调用 `POST <server>/v1/onboard/flybot`,创建家(`@<name>.home`),凭证写在 `$DSH_HOME/bot/flybot-social.json`(权限 600)。
- **成员。** 每个**已发布**的本地 Bot 在服务器上是 `@<bot>.<home>`。Chief 默认已发布,其他 Bot 在你发布后加入,删除会撤销。未发布的 Bot 没有对外身份:朋友够不到它,它也不能给朋友发消息。
- **成员。** 访问是按 Bot 的**授权**(默认拒绝,Chief 也一样)。本地 Bot 在你邀请别人使用它、它加入邀请、或你把它带进面对面房间时,才会在服务器上成为 `@<bot>.<home>`。**「好友的 Bot」** 只列出别人授权给你的 Bot。每个 Bot 的 **管理访问** 列出谁有它,并可 **撤销**。删除 Bot 会撤销其身份。未授权的 Bot 没有对外身份:朋友够不到它,它也不能给朋友发消息。
- **「好友的 Bot」侧栏。** 按家分组,带远端标记;点开就是普通对话。本地 Bot 右键 → **复制邀请链接**(或带说明的邀请),或用 **+ 添加** 粘贴好友的链接加入。
- **审批。** 好友发来的消息会立刻送达。你的 Bot 发出的回复可能停在 **「好友的 Bot」**(角标 + 批准 / 拒绝),除非对该好友开了自动接受。FlyBot 本地也会暂扣 Bot 自己发起的发送;文件一律先审批。
- **工具。** 每个本地 Bot 都有 `list_friend_bots` 与 `message_friend_bot`(可带附件)。
Expand Down Expand Up @@ -113,7 +113,7 @@ export PORT=8787
displayName: 你的名字
```

首次启动会登记你家和每个已发布的本地 Bot(默认是 Chief)。若入驻因家句柄已被占用而失败(HTTP 409),**「好友的 Bot」** 会写出被占用的句柄,并提示在上述配置里改 `displayName` 后重启。添加好友:右键某个 Bot → **复制邀请链接**,对方在 **「好友的 Bot」→ + 添加** 里粘贴。
首次启动只登记你家。在你授权之前,没有任何 Bot 被共享。若入驻因家句柄已被占用而失败(HTTP 409),**「好友的 Bot」** 会写出被占用的句柄,并提示在上述配置里改 `displayName` 后重启。授权某个 Bot:右键该 Bot → **邀请使用此 Bot**,把链接发给对方;对方在 **「好友的 Bot」→ + 添加** 里粘贴。接受邀请只授权这一个 Bot。

好友发来的消息会立刻送达。你的 Bot 回过去的内容可能停在 **「好友的 Bot」** 里等你批准(或拒绝),除非对该好友开了自动接受。如果对方只看见自己的话,先看侧栏的 **待批准** 角标。

Expand Down
20 changes: 17 additions & 3 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -965,7 +965,8 @@ var HOST_ZH = {
"Only a room admin can remove participants": "只有房间管理员可以移除成员",
"Specialists stay in their owner's private rooms; a cross-owner room takes each side's Chief": "专员只待在其主人的私有房间里;跨主人房间只接受各方的 Chief",
"Only a designated Chief of a room member may join a shared room": "只有房间成员的指定 Chief 可以加入共享房间",
"Only a Chief or a Bot published to friends may join a shared room": "只有 Chief 或主人已向好友公开的 Bot 可以加入共享房间",
"Only a Bot granted to you may join a shared room": "只有已授权给你的 Bot 可以加入共享房间",
"Only a Chief or a Bot published to friends may join a shared room": "只有已授权给你的 Bot 可以加入共享房间",
"Participant kind must be human or bot": "成员类型只能是 human 或 bot",
"A participant needs an id": "成员需要 id",
"Room role must be admin or member": "房间角色只能是 admin 或 member",
Expand Down Expand Up @@ -1147,7 +1148,7 @@ var call = async (endpoint, payload, signal) => {
};

// src/client/flybot-registry.js
var SLOTS = ["flybot.sidebar.section", "flybot.sidebar.structure", "flybot.composer.mention", "flybot.bot.details", "flybot.approvals", "flybot.room.overview"];
var SLOTS = ["flybot.sidebar.section", "flybot.sidebar.structure", "flybot.composer.mention", "flybot.bot.details", "flybot.approvals", "flybot.room.overview", "flybot.session.dock"];
function createRegistry() {
const entries2 = new Map(SLOTS.map((name) => [name, []]));
const listeners2 = /* @__PURE__ */ new Set();
Expand Down Expand Up @@ -1360,6 +1361,18 @@ function FlyBotSections({ roster, actions, renderBot }) {
(0, import_react.createElement)(Safe, { entry, props: { roster, actions, call: call2, renderBot, remoteChats } })
)));
}
function FlyBotSessionDock({ sessionId, useRoster, actions }) {
const entries2 = useSlot("flybot.session.dock");
const roster = typeof useRoster === "function" ? useRoster((value) => value) : void 0;
if (entries2.length === 0) return null;
const room2 = roster?.roomsById?.[sessionId];
const bot = room2 ? void 0 : roster?.byId?.[sessionId];
return (0, import_react.createElement)(import_react.Fragment, null, entries2.map((entry) => (0, import_react.createElement)(
"div",
{ key: entry.id, className: "bt-flybot-session-dock", "data-flybot": entry.id },
(0, import_react.createElement)(Safe, { entry, props: { sessionId, roster, room: room2, bot, actions, call: call2 } })
)));
}
function FlyBotRoomOverview({ room: room2, roster, actions }) {
const entries2 = useSlot("flybot.room.overview");
if (entries2.length === 0) return null;
Expand Down Expand Up @@ -14882,7 +14895,8 @@ function apply(ctx) {
shadow("conversation.input.dock", { id: "bot-members", order: -120, inject: face }, MemberDock),
shadow("conversation.input.dock", { id: "bot-model", order: -110, inject: face }, ModelChoiceDock),
shadow("conversation.input.dock", { id: "bot-question", order: -100, inject: face }, QuestionDock),
shadow("conversation.input.dock", { id: "bot-secret", order: -90, inject: face }, SecretDock)
shadow("conversation.input.dock", { id: "bot-secret", order: -90, inject: face }, SecretDock),
shadow("conversation.input.dock", { id: "flybot-session", order: -80, inject: face }, FlyBotSessionDock)
];
ctx.effect(() => {
const style = installStyles(RETURN_CSS, "return");
Expand Down
2 changes: 1 addition & 1 deletion core/src/client/flybot-registry.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// The FlyBot slot registry, free of React so the "@" menu and tests can use it.
// See flybot-slots.js for what each slot carries.
export const SLOTS = ['flybot.sidebar.section', 'flybot.sidebar.structure', 'flybot.composer.mention', 'flybot.bot.details', 'flybot.approvals', 'flybot.room.overview']
export const SLOTS = ['flybot.sidebar.section', 'flybot.sidebar.structure', 'flybot.composer.mention', 'flybot.bot.details', 'flybot.approvals', 'flybot.room.overview', 'flybot.session.dock']

export function createRegistry() {
const entries = new Map(SLOTS.map(name => [name, []]))
Expand Down
11 changes: 11 additions & 0 deletions core/src/client/flybot-slots.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// flybot.bot.details { id, order, component({ bot, roster, call }) } in a Bot's settings
// flybot.approvals { id, order, component({ approval, decide, call }) } custom card for a held send
// flybot.room.overview { id, order, component({ room, roster, actions, call }) } top of a group's overview
// flybot.session.dock { id, order, component({ sessionId, roster, room, bot, actions, call }) } above the composer
//
// Types: ds-bot/types/flybot.d.ts (FlyBotClientRegistry).
import { Component, createElement as h, Fragment, useEffect, useState } from 'react'
Expand Down Expand Up @@ -76,6 +77,16 @@ export function FlyBotSections({ roster, actions, renderBot }) {
h(Safe, { entry, props: { roster, actions, call, renderBot, remoteChats } }))))
}

export function FlyBotSessionDock({ sessionId, useRoster, actions }) {
const entries = useSlot('flybot.session.dock')
const roster = typeof useRoster === 'function' ? useRoster(value => value) : undefined
if (entries.length === 0) return null
const room = roster?.roomsById?.[sessionId]
const bot = room ? undefined : roster?.byId?.[sessionId]
return h(Fragment, null, entries.map(entry => h('div', { key: entry.id, className: 'bt-flybot-session-dock', 'data-flybot': entry.id },
h(Safe, { entry, props: { sessionId, roster, room, bot, actions, call } }))))
}

export function FlyBotRoomOverview({ room, roster, actions }) {
const entries = useSlot('flybot.room.overview')
if (entries.length === 0) return null
Expand Down
3 changes: 2 additions & 1 deletion core/src/client/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -918,7 +918,8 @@ const HOST_ZH = {
'Only a room admin can remove participants': '只有房间管理员可以移除成员',
'Specialists stay in their owner\'s private rooms; a cross-owner room takes each side\'s Chief': '专员只待在其主人的私有房间里;跨主人房间只接受各方的 Chief',
'Only a designated Chief of a room member may join a shared room': '只有房间成员的指定 Chief 可以加入共享房间',
'Only a Chief or a Bot published to friends may join a shared room': '只有 Chief 或主人已向好友公开的 Bot 可以加入共享房间',
'Only a Bot granted to you may join a shared room': '只有已授权给你的 Bot 可以加入共享房间',
'Only a Chief or a Bot published to friends may join a shared room': '只有已授权给你的 Bot 可以加入共享房间',
'Participant kind must be human or bot': '成员类型只能是 human 或 bot',
'A participant needs an id': '成员需要 id',
'Room role must be admin or member': '房间角色只能是 admin 或 member',
Expand Down
Loading
Loading