From f5e8bfc82f0983e726a55c9ffdf96da6ca02c70a Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Wed, 14 Oct 2026 20:29:13 +0800 Subject: [PATCH] fix(remote-chat): reopen a friend's Bot's chat right after it is deleted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deleting a friend's Bot's chat removed its Bot record, and a new chat was only opened when the plugin next published its roster (cached, so often not until a restart). Meanwhile the Friends' Bots section had a published Bot with no chat and drew 'opening its chat…' that never resolved. When a remote chat is deleted and its Bot is still published, open a fresh, empty chat at once. --- ds-bot/src/host/hooks.js | 13 ++++++++++++- ds-bot/test/flybot.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/ds-bot/src/host/hooks.js b/ds-bot/src/host/hooks.js index a599e9223..95589a875 100644 --- a/ds-bot/src/host/hooks.js +++ b/ds-bot/src/host/hooks.js @@ -154,9 +154,20 @@ export function install(rt) { rt.deleteBot = async (id, ...rest) => { // Fires after removal; the payload keeps the Bot's last record, fields included. const last = view(rt.botOf(id)) - const remoteChat = rt.botOf(id)?.remote !== undefined + const remote = rt.botOf(id)?.remote + const remoteChat = remote !== undefined const result = await deleteBot(id, ...rest) if (!remoteChat) fire('bot.removed', { ...last, botId: id, bot: last }) + // Deleting a friend's Bot's chat clears it; the friend's Bot is still there, so it gets a + // fresh, empty chat at once. Otherwise its row waits ("opening its chat…") until the + // plugin next publishes its roster, which may be hours or the next restart. + else if (typeof remote?.handle === 'string' && typeof remote.plugin === 'string') { + const published = (state.flybot?.remote?.[remote.plugin] ?? []).find(bot => bot.handle === remote.handle) + if (published !== undefined) { + await rt.remoteChats?.open({ handle: published.handle, name: published.name, home: published.home, homeName: published.homeName, plugin: remote.plugin }) + .catch(error => warn('flybot: chat for %s not reopened: %s', remote.handle, error)) + } + } return result } } diff --git a/ds-bot/test/flybot.test.js b/ds-bot/test/flybot.test.js index c124a2fa2..6b459a330 100644 --- a/ds-bot/test/flybot.test.js +++ b/ds-bot/test/flybot.test.js @@ -373,3 +373,28 @@ test('tools (#4): a tool visibleTo hides is taken out of that Bot\'s tool list, await new Promise(resolve => setTimeout(resolve, 20)) assert.deepEqual(masks.at(-1), ['c', 'lifted']) }) + +test('deleting a friend\'s Bot\'s chat reopens a fresh one at once, so its row never waits on "opening its chat…"', async (t) => { + const { ctx, fb, call, stop } = await start() + t.after(stop) + // The relay route serves the friend's Bot, as remote-chat.js's adapter does in DSH. + ctx.llm.listProviders = () => [{ id: 'flybot-remote', name: 'Friends\' Bots' }] + ctx.llm.listModels = async () => [{ id: '@helper.friend.home', name: '助手' }] + let sessions = 0 + ctx.sessionController.create = async () => ({ sessionId: `remote-${++sessions}` }) + const social = fb.scope('social') + social.remote.publish([{ handle: '@helper.friend.home', name: '助手', home: '@friend.home' }]) + const until = async (check) => { for (let i = 0; i < 100 && !check(); i++) await new Promise(resolve => setTimeout(resolve, 10)) } + await until(() => social.remote.chats.of('@helper.friend.home')) + const first = social.remote.chats.of('@helper.friend.home') + assert.ok(first) + const deleted = await call('delete-bot', { id: first }) + assert.equal(deleted.ok, true) + // No new publish: the plugin's roster is unchanged and cached. + await until(() => social.remote.chats.of('@helper.friend.home')) + const again = social.remote.chats.list().find(chat => chat.handle === '@helper.friend.home') + assert.ok(again, 'the chat is back without a new publish') + assert.notEqual(again.id, first) + assert.equal(again.name, '助手') + assert.equal(again.plugin, 'social') +})