Merge pull request #4461 from yardbirds0/feat/account-homepage-link
feat: 支持从 API Key 账号名称跳转上游站点主页
This commit is contained in:
@@ -252,7 +252,24 @@
|
||||
</template>
|
||||
<template #cell-name="{ row, value }">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ value }}</span>
|
||||
<HelpTooltip
|
||||
v-if="accountHomepageUrl(row)"
|
||||
:content="accountHomepageUrl(row)"
|
||||
width-class="w-max max-w-sm break-all"
|
||||
class="-ml-1 self-start"
|
||||
>
|
||||
<template #trigger>
|
||||
<a
|
||||
:href="accountHomepageUrl(row)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="border-b border-dotted border-gray-300 font-medium text-gray-900 dark:border-gray-600 dark:text-white"
|
||||
>
|
||||
{{ value }}
|
||||
</a>
|
||||
</template>
|
||||
</HelpTooltip>
|
||||
<span v-else class="font-medium text-gray-900 dark:text-white">{{ value }}</span>
|
||||
<span
|
||||
v-if="accountDisplayEmail(row)"
|
||||
class="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]"
|
||||
@@ -523,6 +540,7 @@ import { buildOpenAIUsageRefreshKey } from '@/utils/accountUsageRefresh'
|
||||
import { formatDateTime, formatRelativeTime } from '@/utils/format'
|
||||
import { proxyExpiryBadgeClass, proxyExpiryLabelKey } from '@/utils/proxyExpiry'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import { sanitizeUrl } from '@/utils/url'
|
||||
import type { Account, AccountPlatform, AccountSchedulerGroupScore, AccountType, Proxy as AccountProxy, AdminGroup, WindowStats, ClaudeModel, UpstreamBillingProbeSettings, UpstreamBillingProbeSnapshot } from '@/types'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -1295,6 +1313,12 @@ function accountDisplayEmail(row: any): string {
|
||||
return row.extra?.email_address || row.extra?.email || row.credentials?.email || row.parent_email || ''
|
||||
}
|
||||
|
||||
function accountHomepageUrl(row: Account): string {
|
||||
if (row.type !== 'apikey' || typeof row.credentials?.base_url !== 'string') return ''
|
||||
const baseUrl = sanitizeUrl(row.credentials.base_url)
|
||||
return baseUrl ? new URL(baseUrl).origin : ''
|
||||
}
|
||||
|
||||
type OpenAICompactBadgeState = 'active' | 'blocked' | 'auto'
|
||||
|
||||
function getOpenAICompactState(row: any): OpenAICompactBadgeState | null {
|
||||
|
||||
@@ -5,6 +5,7 @@ import AccountsView from '../AccountsView.vue'
|
||||
import AccountActionMenu from '@/components/admin/account/AccountActionMenu.vue'
|
||||
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
|
||||
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
||||
import HelpTooltip from '@/components/common/HelpTooltip.vue'
|
||||
|
||||
// 外审 F2:AccountActionMenu emit 'create-spark-shadow',但 AccountsView 此前未监听,
|
||||
// 导致按钮点击无效。本测试通过真实组件引用 emit 该事件,断言父页面接线调用 API。
|
||||
@@ -207,7 +208,7 @@ describe('admin AccountsView — 外审 F2:spark 影子创建接线', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Task 6: 影子行 parent_* OR 兜底展示
|
||||
// 账号行展示
|
||||
const mountViewWithRow = () =>
|
||||
mount(AccountsView, {
|
||||
global: {
|
||||
@@ -255,7 +256,7 @@ const mountViewWithRow = () =>
|
||||
}
|
||||
})
|
||||
|
||||
describe('admin AccountsView — 影子行 parent_* OR 兜底展示', () => {
|
||||
describe('admin AccountsView — 账号行展示', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
for (const fn of [listAccounts, listWithEtag, getBatchTodayStats, getAllProxies, getAllGroups, duplicateAccount, createSparkShadow, showSuccess, showError]) {
|
||||
@@ -304,6 +305,47 @@ describe('admin AccountsView — 影子行 parent_* OR 兜底展示', () => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('仅将具有安全 base_url 的 API Key 账号名称链接到站点主页', async () => {
|
||||
listAccounts.mockResolvedValue({
|
||||
items: [
|
||||
{ id: 101, name: 'relay-account', platform: 'openai', type: 'apikey', credentials: { base_url: 'https://relay.example.com/api/v1/' } },
|
||||
{ id: 102, name: 'oauth-account', platform: 'openai', type: 'oauth', credentials: { base_url: 'https://oauth.example.com/v1' } },
|
||||
{ id: 103, name: 'invalid-url', platform: 'openai', type: 'apikey', credentials: { base_url: 'javascript:alert(1)' } },
|
||||
],
|
||||
total: 3,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
pages: 1,
|
||||
})
|
||||
|
||||
const wrapper = mountViewWithRow()
|
||||
await flushPromises()
|
||||
|
||||
const links = wrapper.findAll('a')
|
||||
expect(links).toHaveLength(1)
|
||||
const [link] = links
|
||||
expect(link.text()).toBe('relay-account')
|
||||
expect(link.attributes()).toMatchObject({
|
||||
href: 'https://relay.example.com',
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
})
|
||||
expect(link.classes()).toEqual(expect.arrayContaining([
|
||||
'border-dotted',
|
||||
'text-gray-900',
|
||||
'dark:text-white',
|
||||
]))
|
||||
expect(link.classes()).not.toContain('text-primary-600')
|
||||
const tooltip = wrapper.findComponent(HelpTooltip)
|
||||
expect(tooltip.props('content')).toBe('https://relay.example.com')
|
||||
expect(tooltip.props('widthClass')).toBe('w-max max-w-sm break-all')
|
||||
expect(tooltip.classes()).toEqual(expect.arrayContaining(['self-start']))
|
||||
expect(wrapper.text()).toContain('oauth-account')
|
||||
expect(wrapper.text()).toContain('invalid-url')
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('passes fresh Grok billing and quota snapshots before stale credential fallbacks', async () => {
|
||||
const grokAccounts = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user