mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
fix(desktop): broken "Open setup guide" button for plugin platforms
On the desktop Channels / Messaging page, the "Open setup guide" button was
rendered as a bare <a href={platform.docs_url} target="_blank"> with no guard.
Plugin-provided platforms (Microsoft Teams, Google Chat, Line, Raft, Yuanbao,
…) ship an empty docs_url, so the anchor's href was "".
In a packaged build, Electron resolves an empty href against the current
document — the app's own index.html inside the asar bundle — and
shell.openPath then fails with an OS "file not found" dialog. This is exactly
the Windows error reported for Messaging → Teams → Open guide.
Fix (3 changes):
1. fix(desktop) — Only render the "Open setup guide" button when docs_url is
non-empty, and route clicks through openExternalLink so a relative/empty
value can never be treated as a local bundle path. Fixes the whole class
(every plugin platform), not just Teams.
2. fix(messaging) — Give the Teams platform plugin a real docs_url (Microsoft
Teams setup guide) so its card shows a working button instead of nothing.
3. fix(messaging) — Give the Google Chat platform plugin a real docs_url
(Google Chat setup guide) so its card shows a working button instead of
nothing. Originally from #48940; folded in here because that PR's test
was broken (it queried the HTTP endpoint, but google_chat is a dynamic
enum member that only appears after the adapter module is imported).
Test plan:
- apps/desktop — new src/app/messaging/index.test.tsx: button is hidden when
docs_url is empty; a real URL opens via the validated external opener (does
not navigate).
- apps/desktop typecheck (tsc --noEmit) clean.
- backend — test_teams_messaging_metadata_links_setup_guide: the Teams catalog
entry exposes the setup-guide docs_url.
- backend — test_google_chat_messaging_metadata_links_setup_guide: the Google
Chat catalog entry exposes the setup-guide docs_url.
Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
Co-authored-by: p-andhika <andhika.prakasiwi@gmail.com>
This commit is contained in:
parent
58919f68ab
commit
244a6f2ceb
4 changed files with 152 additions and 8 deletions
89
apps/desktop/src/app/messaging/index.test.tsx
Normal file
89
apps/desktop/src/app/messaging/index.test.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// @vitest-environment jsdom
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { MessagingPlatformInfo } from '@/types/hermes'
|
||||
|
||||
const getMessagingPlatforms = vi.fn()
|
||||
const updateMessagingPlatform = vi.fn()
|
||||
const openExternalLink = vi.fn()
|
||||
|
||||
vi.mock('@/hermes', () => ({
|
||||
getMessagingPlatforms: () => getMessagingPlatforms(),
|
||||
updateMessagingPlatform: (id: string, body: unknown) => updateMessagingPlatform(id, body)
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/external-link', () => ({
|
||||
openExternalLink: (href: string) => openExternalLink(href)
|
||||
}))
|
||||
|
||||
vi.mock('@/store/notifications', () => ({
|
||||
notify: vi.fn(),
|
||||
notifyError: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/store/system-actions', () => ({
|
||||
runGatewayRestart: vi.fn()
|
||||
}))
|
||||
|
||||
function platform(patch: Partial<MessagingPlatformInfo> = {}): MessagingPlatformInfo {
|
||||
return {
|
||||
configured: false,
|
||||
description: 'A platform.',
|
||||
docs_url: '',
|
||||
enabled: false,
|
||||
env_vars: [],
|
||||
gateway_running: true,
|
||||
id: 'teams',
|
||||
name: 'Microsoft Teams',
|
||||
state: 'disabled',
|
||||
...patch
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
updateMessagingPlatform.mockResolvedValue({ ok: true, platform: 'teams' })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
async function renderMessaging() {
|
||||
const { MessagingView } = await import('./index')
|
||||
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<MessagingView />
|
||||
</MemoryRouter>
|
||||
)
|
||||
}
|
||||
|
||||
describe('MessagingView setup-guide link', () => {
|
||||
it('hides the setup-guide button for a plugin platform with no docs URL', async () => {
|
||||
// Teams (and other plugin platforms) ship an empty docs_url. Rendering an
|
||||
// anchor with href="" let Electron resolve it to the app's own packaged
|
||||
// index.html and fail with an OS "file not found" dialog. The button must
|
||||
// simply not appear when there is no guide to open.
|
||||
getMessagingPlatforms.mockResolvedValue({ platforms: [platform({ docs_url: '' })] })
|
||||
|
||||
await renderMessaging()
|
||||
|
||||
expect((await screen.findAllByText('Microsoft Teams')).length).toBeGreaterThan(0)
|
||||
expect(screen.queryByText('Open setup guide')).toBeNull()
|
||||
})
|
||||
|
||||
it('opens a real docs URL through the validated external opener', async () => {
|
||||
const docsUrl = 'https://hermes-agent.nousresearch.com/docs/user-guide/messaging/teams'
|
||||
getMessagingPlatforms.mockResolvedValue({ platforms: [platform({ docs_url: docsUrl })] })
|
||||
|
||||
await renderMessaging()
|
||||
|
||||
const link = await screen.findByText('Open setup guide')
|
||||
fireEvent.click(link)
|
||||
|
||||
await waitFor(() => expect(openExternalLink).toHaveBeenCalledWith(docsUrl))
|
||||
})
|
||||
})
|
||||
|
|
@ -14,6 +14,7 @@ import {
|
|||
updateMessagingPlatform
|
||||
} from '@/hermes'
|
||||
import { type Translations, useI18n } from '@/i18n'
|
||||
import { openExternalLink } from '@/lib/external-link'
|
||||
import { AlertTriangle, ExternalLink, Save, Trash2 } from '@/lib/icons'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { notify, notifyError } from '@/store/notifications'
|
||||
|
|
@ -404,14 +405,31 @@ function PlatformDetail({
|
|||
<p className="mt-1 text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">
|
||||
{introCopy(platform, m)}
|
||||
</p>
|
||||
<div className="mt-3">
|
||||
<Button asChild size="sm" variant="textStrong">
|
||||
<a href={platform.docs_url} rel="noreferrer" target="_blank">
|
||||
{m.openSetupGuide}
|
||||
<ExternalLink className="size-3.5" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
{platform.docs_url && (
|
||||
<div className="mt-3">
|
||||
<Button asChild size="sm" variant="textStrong">
|
||||
<a
|
||||
href={platform.docs_url}
|
||||
onClick={event => {
|
||||
// Route through the validated external opener instead of
|
||||
// letting Electron resolve the anchor. A packaged build's
|
||||
// empty/relative href resolves to the app's own
|
||||
// index.html file path, which shell.openPath then fails to
|
||||
// open ("file not found"). Plugin platforms (Teams, etc.)
|
||||
// ship no docs_url, so this guard + handler keeps the
|
||||
// button from ever pointing at a local bundle path.
|
||||
event.preventDefault()
|
||||
openExternalLink(platform.docs_url)
|
||||
}}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{m.openSetupGuide}
|
||||
<ExternalLink className="size-3.5" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
|
|
|||
|
|
@ -4749,6 +4749,11 @@ _PLATFORM_OVERRIDES: dict[str, dict[str, Any]] = {
|
|||
),
|
||||
"required_env": ("FEISHU_APP_ID", "FEISHU_APP_SECRET"),
|
||||
},
|
||||
"google_chat": {
|
||||
"name": "Google Chat",
|
||||
"description": "Connect Hermes to Google Chat via Cloud Pub/Sub.",
|
||||
"docs_url": "https://hermes-agent.nousresearch.com/docs/user-guide/messaging/google_chat",
|
||||
},
|
||||
"wecom": {
|
||||
"name": "WeCom (group bot)",
|
||||
"description": "Send-only WeCom group bot via webhook.",
|
||||
|
|
@ -4798,6 +4803,12 @@ _PLATFORM_OVERRIDES: dict[str, dict[str, Any]] = {
|
|||
"env_vars": ("QQ_APP_ID", "QQ_CLIENT_SECRET", "QQ_ALLOWED_USERS"),
|
||||
"required_env": ("QQ_APP_ID", "QQ_CLIENT_SECRET"),
|
||||
},
|
||||
# Teams ships as a platform plugin, so its name/env vars come from the
|
||||
# plugin registry. Only the docs link needs an override here so the
|
||||
# Channels page can point at the Microsoft Teams setup guide.
|
||||
"teams": {
|
||||
"docs_url": "https://hermes-agent.nousresearch.com/docs/user-guide/messaging/teams",
|
||||
},
|
||||
"yuanbao": {
|
||||
"name": "Yuanbao (元宝)",
|
||||
"description": "Connect Hermes to Tencent Yuanbao.",
|
||||
|
|
@ -4842,6 +4853,7 @@ _PLATFORM_ORDER: tuple[str, ...] = (
|
|||
"sms",
|
||||
"dingtalk",
|
||||
"feishu",
|
||||
"google_chat",
|
||||
"wecom",
|
||||
"wecom_callback",
|
||||
"weixin",
|
||||
|
|
|
|||
|
|
@ -1746,6 +1746,31 @@ class TestWebServerEndpoints:
|
|||
assert "QR login" in fields[key]["description"]
|
||||
assert "Official Account" not in fields[key]["description"]
|
||||
|
||||
def test_teams_messaging_metadata_links_setup_guide(self):
|
||||
# Teams is a platform plugin, so the catalog entry is built from the
|
||||
# plugin registry. The override must still supply a docs link so the
|
||||
# Channels page renders a working "Open setup guide" button instead of
|
||||
# an empty href (which resolves to the packaged app's own index.html).
|
||||
from hermes_cli.web_server import _build_catalog_entry
|
||||
|
||||
teams = _build_catalog_entry("teams")
|
||||
assert teams["docs_url"] == (
|
||||
"https://hermes-agent.nousresearch.com/docs/user-guide/messaging/teams"
|
||||
)
|
||||
|
||||
def test_google_chat_messaging_metadata_links_setup_guide(self):
|
||||
# Google Chat is a platform plugin, so the catalog entry is built from
|
||||
# the plugin registry. The override must supply a docs link so the
|
||||
# Channels page renders a working "Open setup guide" button instead of
|
||||
# an empty href (which resolves to the packaged app's own index.html).
|
||||
from hermes_cli.web_server import _build_catalog_entry
|
||||
|
||||
google_chat = _build_catalog_entry("google_chat")
|
||||
assert google_chat["name"] == "Google Chat"
|
||||
assert google_chat["docs_url"] == (
|
||||
"https://hermes-agent.nousresearch.com/docs/user-guide/messaging/google_chat"
|
||||
)
|
||||
|
||||
def test_messaging_catalog_covers_gateway_platforms(self):
|
||||
"""Catalog is derived from the Platform enum, so every built-in shows up."""
|
||||
from gateway.config import Platform
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue