Make H5 browser access manageable from desktop settings

Add the desktop-side H5 settings client, store state, and General tab controls so users can opt in, regenerate tokens, manage origins, and copy the browser URL without touching unrelated desktop flows.

Constraint: Task 3 is limited to desktop Settings UI and store wiring only
Rejected: Expand into AppShell or browser runtime work now | reserved for later tasks in the plan
Confidence: high
Scope-risk: narrow
Directive: Keep H5 fetchAll integration tolerant of missing endpoint responses so existing settings loads do not regress
Tested: cd desktop && bun run test -- src/__tests__/generalSettings.test.tsx src/stores/settingsStore.test.ts
Tested: cd desktop && bun run lint
This commit is contained in:
程序员阿江(Relakkes) 2026-05-09 23:57:52 +08:00
parent 37bdc4e6e7
commit a677163e0d
7 changed files with 474 additions and 2 deletions

View File

@ -126,6 +126,13 @@ describe('Settings > General tab', () => {
skipWebFetchPreflight: true,
desktopNotificationsEnabled: true,
webSearch: { mode: 'auto', tavilyApiKey: '', braveApiKey: '' },
h5Access: {
enabled: false,
tokenPreview: null,
allowedOrigins: [],
publicBaseUrl: null,
},
h5AccessGeneratedToken: null,
setThinkingEnabled: vi.fn().mockImplementation(async (enabled: boolean) => {
useSettingsStore.setState({ thinkingEnabled: enabled })
}),
@ -138,6 +145,11 @@ describe('Settings > General tab', () => {
setWebSearch: vi.fn().mockImplementation(async (webSearch) => {
useSettingsStore.setState({ webSearch })
}),
enableH5Access: vi.fn(),
disableH5Access: vi.fn(),
regenerateH5AccessToken: vi.fn(),
updateH5AccessSettings: vi.fn(),
clearH5AccessGeneratedToken: vi.fn(),
})
useUIStore.setState({ pendingSettingsTab: null })
@ -237,6 +249,81 @@ describe('Settings > General tab', () => {
})
})
it('renders the H5 section in a disabled state by default', () => {
render(<Settings />)
fireEvent.click(screen.getByText('General'))
const section = screen.getByRole('region', { name: 'H5 Access' })
expect(within(section).getByLabelText('Enable H5 access')).not.toBeChecked()
expect(within(section).getByText('Disabled')).toBeInTheDocument()
})
it('enables H5 access from the General settings section', async () => {
render(<Settings />)
fireEvent.click(screen.getByText('General'))
const section = screen.getByRole('region', { name: 'H5 Access' })
await act(async () => {
fireEvent.click(within(section).getByLabelText('Enable H5 access'))
})
expect(useSettingsStore.getState().enableH5Access).toHaveBeenCalledTimes(1)
})
it('regenerates the H5 token from General settings', async () => {
useSettingsStore.setState({
h5Access: {
enabled: true,
tokenPreview: 'h5a1b2c3',
allowedOrigins: ['https://phone.example'],
publicBaseUrl: 'https://phone.example/app',
},
})
render(<Settings />)
fireEvent.click(screen.getByText('General'))
const section = screen.getByRole('region', { name: 'H5 Access' })
await act(async () => {
fireEvent.click(within(section).getByRole('button', { name: 'Regenerate token' }))
})
expect(useSettingsStore.getState().regenerateH5AccessToken).toHaveBeenCalledTimes(1)
})
it('updates H5 public URL and allowed origins from General settings', async () => {
useSettingsStore.setState({
h5Access: {
enabled: true,
tokenPreview: 'h5a1b2c3',
allowedOrigins: ['https://old.example'],
publicBaseUrl: null,
},
})
render(<Settings />)
fireEvent.click(screen.getByText('General'))
const section = screen.getByRole('region', { name: 'H5 Access' })
fireEvent.change(within(section).getByLabelText('Public URL'), {
target: { value: 'https://phone.example/app' },
})
fireEvent.change(within(section).getByLabelText('Allowed origins'), {
target: { value: 'https://phone.example, https://tablet.example' },
})
await act(async () => {
fireEvent.click(within(section).getByRole('button', { name: 'Save H5 settings' }))
})
expect(useSettingsStore.getState().updateH5AccessSettings).toHaveBeenCalledWith({
publicBaseUrl: 'https://phone.example/app',
allowedOrigins: ['https://phone.example', 'https://tablet.example'],
})
})
it('saves WebSearch fallback provider settings', () => {
render(<Settings />)

View File

@ -0,0 +1,38 @@
import { api } from './client'
import type { H5AccessSettings } from '../types/settings'
export type { H5AccessSettings } from '../types/settings'
export type H5AccessStatus = {
settings: H5AccessSettings
}
export type H5AccessTokenResult = {
settings: H5AccessSettings
token: string
}
export const h5AccessApi = {
get() {
return api.get<H5AccessStatus>('/api/h5-access')
},
enable() {
return api.post<H5AccessTokenResult>('/api/h5-access/enable')
},
disable() {
return api.post<H5AccessStatus>('/api/h5-access/disable')
},
regenerate() {
return api.post<H5AccessTokenResult>('/api/h5-access/regenerate')
},
update(input: {
allowedOrigins?: string[]
publicBaseUrl?: string | null
}) {
return api.put<H5AccessStatus>('/api/h5-access', input)
},
}

View File

@ -667,6 +667,26 @@ export const en = {
'settings.general.notificationsOpenSettings': 'Open Settings',
'settings.general.notificationsTestTitle': 'Claude Code Haha notifications are enabled',
'settings.general.notificationsTestBody': 'Permission prompts and completed agent replies will now use system notifications.',
'settings.general.h5AccessTitle': 'H5 Access',
'settings.general.h5AccessDescription': 'Opt in to browser access from your phone or another device using a generated token and an allowlisted origin set.',
'settings.general.h5AccessEnabled': 'Enable H5 access',
'settings.general.h5AccessEnabledHint': 'Turn this on only for networks and browser origins you control.',
'settings.general.h5AccessTokenPreview': 'Token preview',
'settings.general.h5AccessDisabledValue': 'Disabled',
'settings.general.h5AccessRegenerate': 'Regenerate token',
'settings.general.h5AccessGeneratedToken': 'Generated token',
'settings.general.h5AccessGeneratedTokenHint': 'Shown once. Copy it now and store it like a password.',
'settings.general.h5AccessCopy': 'Copy',
'settings.general.h5AccessHideToken': 'Hide token',
'settings.general.h5AccessPublicUrl': 'Public URL',
'settings.general.h5AccessPublicUrlPlaceholder': 'https://chat.example.com',
'settings.general.h5AccessAllowedOrigins': 'Allowed origins',
'settings.general.h5AccessAllowedOriginsPlaceholder': 'https://chat.example.com, https://phone.example',
'settings.general.h5AccessOriginsHint': 'Enter one origin per line or separate multiple origins with commas.',
'settings.general.h5AccessSave': 'Save H5 settings',
'settings.general.h5AccessUrl': 'H5 URL',
'settings.general.h5AccessSafetyNote': 'Treat the full token like a password and only allow origins you fully control.',
'settings.general.h5AccessError': 'Failed to update H5 access settings.',
'settings.general.webFetchPreflightTitle': 'WebFetch Preflight',
'settings.general.webFetchPreflightDescription': 'Desktop sessions skip Claude\'s domain preflight by default to avoid false failures on third-party providers and restricted networks.',
'settings.general.webFetchPreflightEnabled': 'Skip WebFetch domain preflight',

View File

@ -669,6 +669,26 @@ export const zh: Record<TranslationKey, string> = {
'settings.general.notificationsOpenSettings': '打开系统设置',
'settings.general.notificationsTestTitle': 'Claude Code Haha 通知已启用',
'settings.general.notificationsTestBody': '后续授权确认和 Agent 回复完成都会通过系统通知提醒。',
'settings.general.h5AccessTitle': 'H5 访问',
'settings.general.h5AccessDescription': '通过一次性生成的令牌和允许来源列表,为手机或其他设备上的浏览器访问开启可选 H5 模式。',
'settings.general.h5AccessEnabled': '启用 H5 访问',
'settings.general.h5AccessEnabledHint': '只应在你可控的网络和浏览器来源上开启。',
'settings.general.h5AccessTokenPreview': '令牌预览',
'settings.general.h5AccessDisabledValue': '未启用',
'settings.general.h5AccessRegenerate': '重新生成令牌',
'settings.general.h5AccessGeneratedToken': '生成的令牌',
'settings.general.h5AccessGeneratedTokenHint': '仅显示一次,请立即复制并像密码一样保存。',
'settings.general.h5AccessCopy': '复制',
'settings.general.h5AccessHideToken': '隐藏令牌',
'settings.general.h5AccessPublicUrl': '公开访问 URL',
'settings.general.h5AccessPublicUrlPlaceholder': 'https://chat.example.com',
'settings.general.h5AccessAllowedOrigins': '允许的来源',
'settings.general.h5AccessAllowedOriginsPlaceholder': 'https://chat.example.com, https://phone.example',
'settings.general.h5AccessOriginsHint': '每行一个来源,或使用逗号分隔多个来源。',
'settings.general.h5AccessSave': '保存 H5 设置',
'settings.general.h5AccessUrl': 'H5 链接',
'settings.general.h5AccessSafetyNote': '完整令牌等同于密码,只允许你完全信任和可控的来源。',
'settings.general.h5AccessError': '更新 H5 设置失败。',
'settings.general.webFetchPreflightTitle': 'WebFetch 预检',
'settings.general.webFetchPreflightDescription': '桌面端默认跳过 Claude 的域名预检,避免第三方服务商或受限网络下出现误报失败。',
'settings.general.webFetchPreflightEnabled': '跳过 WebFetch 域名预检',

View File

@ -5,6 +5,7 @@ import { useTranslation } from '../i18n'
import { Modal } from '../components/shared/Modal'
import { ConfirmDialog } from '../components/shared/ConfirmDialog'
import { Input } from '../components/shared/Input'
import { Textarea } from '../components/shared/Textarea'
import { Button } from '../components/shared/Button'
import { Dropdown } from '../components/shared/Dropdown'
import type { PermissionMode, EffortLevel, ThemeMode, WebSearchMode } from '../types/settings'
@ -44,6 +45,7 @@ import {
restoreSettingsJsonSecrets,
stripProviderSettingsJsonEnv,
} from '../lib/providerSettingsJson'
import { copyTextToClipboard } from '../components/chat/clipboard'
export function Settings() {
const [activeTab, setActiveTab] = useState<SettingsTab>('providers')
@ -1355,17 +1357,37 @@ function GeneralSettings() {
setDesktopNotificationsEnabled,
webSearch,
setWebSearch,
h5Access,
h5AccessGeneratedToken,
enableH5Access,
disableH5Access,
regenerateH5AccessToken,
updateH5AccessSettings,
clearH5AccessGeneratedToken,
} = useSettingsStore()
const t = useTranslation()
const [webSearchDraft, setWebSearchDraft] = useState(webSearch)
const [h5PublicBaseUrlDraft, setH5PublicBaseUrlDraft] = useState(h5Access.publicBaseUrl ?? '')
const [h5AllowedOriginsDraft, setH5AllowedOriginsDraft] = useState(serializeAllowedOrigins(h5Access.allowedOrigins))
const [notificationPermission, setNotificationPermission] = useState<DesktopNotificationPermission>('default')
const [notificationActionRunning, setNotificationActionRunning] = useState(false)
const [h5ActionRunning, setH5ActionRunning] = useState(false)
const [h5Error, setH5Error] = useState<string | null>(null)
const webSearchDirty = JSON.stringify(webSearchDraft) !== JSON.stringify(webSearch)
const h5AccessUrl = h5Access.enabled && h5Access.publicBaseUrl ? h5Access.publicBaseUrl : null
const h5AccessDirty =
h5PublicBaseUrlDraft.trim() !== (h5Access.publicBaseUrl ?? '') ||
!arraysEqual(parseAllowedOriginsDraft(h5AllowedOriginsDraft), h5Access.allowedOrigins)
useEffect(() => {
setWebSearchDraft(webSearch)
}, [webSearch])
useEffect(() => {
setH5PublicBaseUrlDraft(h5Access.publicBaseUrl ?? '')
setH5AllowedOriginsDraft(serializeAllowedOrigins(h5Access.allowedOrigins))
}, [h5Access])
useEffect(() => {
let cancelled = false
getDesktopNotificationPermission().then((permission) => {
@ -1453,6 +1475,42 @@ function GeneralSettings() {
}
}
const runH5Action = async (action: () => Promise<void>) => {
setH5ActionRunning(true)
setH5Error(null)
try {
await action()
} catch (error) {
setH5Error(error instanceof Error ? error.message : t('settings.general.h5AccessError'))
} finally {
setH5ActionRunning(false)
}
}
const handleH5AccessToggle = async (enabled: boolean) => {
await runH5Action(async () => {
if (enabled) {
await enableH5Access()
return
}
await disableH5Access()
})
}
const handleH5SettingsSave = async () => {
await runH5Action(async () => {
await updateH5AccessSettings({
publicBaseUrl: h5PublicBaseUrlDraft.trim() || null,
allowedOrigins: parseAllowedOriginsDraft(h5AllowedOriginsDraft),
})
})
}
const handleH5Copy = async (value: string) => {
await copyTextToClipboard(value)
}
return (
<div className="max-w-xl">
{/* Appearance selector */}
@ -1580,6 +1638,157 @@ function GeneralSettings() {
</div>
</div>
<div className="mt-8">
<section aria-labelledby="general-h5-access-title" role="region">
<h2
id="general-h5-access-title"
className="text-base font-semibold text-[var(--color-text-primary)] mb-1"
>
{t('settings.general.h5AccessTitle')}
</h2>
<p className="text-sm text-[var(--color-text-tertiary)] mb-3">
{t('settings.general.h5AccessDescription')}
</p>
<div className="rounded-xl border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-4 py-4">
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
aria-label={t('settings.general.h5AccessEnabled')}
checked={h5Access.enabled}
onChange={(event) => void handleH5AccessToggle(event.target.checked)}
disabled={h5ActionRunning}
className="mt-0.5 h-4 w-4 rounded border-[var(--color-border)] text-[var(--color-brand)] focus:ring-[var(--color-brand)]"
/>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-[var(--color-text-primary)]">
{t('settings.general.h5AccessEnabled')}
</div>
<div className="text-xs text-[var(--color-text-tertiary)] mt-1 leading-5">
{t('settings.general.h5AccessEnabledHint')}
</div>
</div>
</label>
<div className="mt-4 border-t border-[var(--color-border)]/60 pt-4">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-xs uppercase tracking-[0.08em] text-[var(--color-text-tertiary)]">
{t('settings.general.h5AccessTokenPreview')}
</div>
<div className="mt-1 text-sm font-medium text-[var(--color-text-primary)]">
{h5Access.tokenPreview ?? t('settings.general.h5AccessDisabledValue')}
</div>
</div>
<Button
size="sm"
variant="secondary"
onClick={() => void runH5Action(regenerateH5AccessToken)}
disabled={!h5Access.enabled || h5ActionRunning}
>
{t('settings.general.h5AccessRegenerate')}
</Button>
</div>
</div>
{h5AccessGeneratedToken && (
<div className="mt-4 border-t border-[var(--color-border)]/60 pt-4">
<div className="flex items-center justify-between gap-3">
<div className="text-sm font-medium text-[var(--color-text-primary)]">
{t('settings.general.h5AccessGeneratedToken')}
</div>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="secondary"
onClick={() => void handleH5Copy(h5AccessGeneratedToken)}
>
{t('settings.general.h5AccessCopy')}
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => clearH5AccessGeneratedToken()}
>
{t('settings.general.h5AccessHideToken')}
</Button>
</div>
</div>
<code className="mt-2 block rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-xs text-[var(--color-text-primary)] break-all">
{h5AccessGeneratedToken}
</code>
<p className="mt-2 text-xs text-[var(--color-text-tertiary)]">
{t('settings.general.h5AccessGeneratedTokenHint')}
</p>
</div>
)}
<div className="mt-4 grid grid-cols-1 gap-3 border-t border-[var(--color-border)]/60 pt-4">
<Input
id="h5-access-public-url"
label={t('settings.general.h5AccessPublicUrl')}
value={h5PublicBaseUrlDraft}
placeholder={t('settings.general.h5AccessPublicUrlPlaceholder')}
onChange={(event) => setH5PublicBaseUrlDraft(event.target.value)}
/>
<Textarea
id="h5-access-allowed-origins"
label={t('settings.general.h5AccessAllowedOrigins')}
value={h5AllowedOriginsDraft}
placeholder={t('settings.general.h5AccessAllowedOriginsPlaceholder')}
onChange={(event) => setH5AllowedOriginsDraft(event.target.value)}
className="min-h-[88px]"
/>
<div className="flex items-center justify-between gap-3">
<p className="text-xs text-[var(--color-text-tertiary)]">
{t('settings.general.h5AccessOriginsHint')}
</p>
<Button
size="sm"
variant="secondary"
onClick={() => void handleH5SettingsSave()}
disabled={!h5AccessDirty || h5ActionRunning}
aria-label={t('settings.general.h5AccessSave')}
>
{t('settings.general.h5AccessSave')}
</Button>
</div>
</div>
{h5AccessUrl && (
<div className="mt-4 border-t border-[var(--color-border)]/60 pt-4">
<div className="flex items-center gap-2">
<div className="flex-1 min-w-0">
<div className="text-xs uppercase tracking-[0.08em] text-[var(--color-text-tertiary)]">
{t('settings.general.h5AccessUrl')}
</div>
<div className="mt-1 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-sm text-[var(--color-text-primary)] break-all">
{h5AccessUrl}
</div>
</div>
<Button
size="sm"
variant="secondary"
className="shrink-0"
onClick={() => void handleH5Copy(h5AccessUrl)}
>
{t('settings.general.h5AccessCopy')}
</Button>
</div>
</div>
)}
<p className="mt-4 text-xs text-[var(--color-text-tertiary)] leading-5">
{t('settings.general.h5AccessSafetyNote')}
</p>
{h5Error && (
<p className="mt-2 text-xs text-[var(--color-error)]">
{h5Error}
</p>
)}
</div>
</section>
</div>
<div className="mt-8">
<h2 className="text-base font-semibold text-[var(--color-text-primary)] mb-1">{t('settings.general.webFetchPreflightTitle')}</h2>
<p className="text-sm text-[var(--color-text-tertiary)] mb-3">{t('settings.general.webFetchPreflightDescription')}</p>
@ -1698,6 +1907,21 @@ function GeneralSettings() {
)
}
function serializeAllowedOrigins(origins: string[]) {
return origins.join(', ')
}
function parseAllowedOriginsDraft(value: string) {
return value
.split(/[\n,]/)
.map((origin) => origin.trim())
.filter(Boolean)
}
function arraysEqual(left: string[], right: string[]) {
return left.length === right.length && left.every((value, index) => value === right[index])
}
// ─── Agents Settings ──────────────────────────────────────
const AGENT_COLORS: Record<string, string> = {

View File

@ -1,7 +1,8 @@
import { create } from 'zustand'
import { settingsApi } from '../api/settings'
import { modelsApi } from '../api/models'
import type { PermissionMode, EffortLevel, ModelInfo, ThemeMode, WebSearchSettings } from '../types/settings'
import { h5AccessApi } from '../api/h5Access'
import type { H5AccessSettings, PermissionMode, EffortLevel, ModelInfo, ThemeMode, WebSearchSettings } from '../types/settings'
import type { Locale } from '../i18n'
import { useUIStore } from './uiStore'
@ -28,10 +29,13 @@ type SettingsStore = {
skipWebFetchPreflight: boolean
desktopNotificationsEnabled: boolean
webSearch: WebSearchSettings
h5Access: H5AccessSettings
h5AccessGeneratedToken: string | null
isLoading: boolean
error: string | null
fetchAll: () => Promise<void>
fetchH5Access: () => Promise<void>
setPermissionMode: (mode: PermissionMode) => Promise<void>
setModel: (modelId: string) => Promise<void>
setEffort: (level: EffortLevel) => Promise<void>
@ -41,6 +45,21 @@ type SettingsStore = {
setSkipWebFetchPreflight: (enabled: boolean) => Promise<void>
setDesktopNotificationsEnabled: (enabled: boolean) => Promise<void>
setWebSearch: (settings: WebSearchSettings) => Promise<void>
enableH5Access: () => Promise<void>
disableH5Access: () => Promise<void>
regenerateH5AccessToken: () => Promise<void>
updateH5AccessSettings: (input: {
allowedOrigins?: string[]
publicBaseUrl?: string | null
}) => Promise<void>
clearH5AccessGeneratedToken: () => void
}
const DEFAULT_H5_ACCESS_SETTINGS: H5AccessSettings = {
enabled: false,
tokenPreview: null,
allowedOrigins: [],
publicBaseUrl: null,
}
export const useSettingsStore = create<SettingsStore>((set, get) => ({
@ -55,18 +74,21 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
skipWebFetchPreflight: true,
desktopNotificationsEnabled: false,
webSearch: { mode: 'auto', tavilyApiKey: '', braveApiKey: '' },
h5Access: DEFAULT_H5_ACCESS_SETTINGS,
h5AccessGeneratedToken: null,
isLoading: false,
error: null,
fetchAll: async () => {
set({ isLoading: true, error: null })
try {
const [{ mode }, modelsRes, { model }, { level }, userSettings] = await Promise.all([
const [{ mode }, modelsRes, { model }, { level }, userSettings, h5AccessRes] = await Promise.all([
settingsApi.getPermissionMode(),
modelsApi.list(),
modelsApi.getCurrent(),
modelsApi.getEffort(),
settingsApi.getUser(),
h5AccessApi.get().catch(() => ({ settings: DEFAULT_H5_ACCESS_SETTINGS })),
])
const theme = userSettings.theme === 'dark' ? 'dark' : 'light'
useUIStore.getState().setTheme(theme)
@ -81,6 +103,8 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
skipWebFetchPreflight: userSettings.skipWebFetchPreflight !== false,
desktopNotificationsEnabled: userSettings.desktopNotificationsEnabled === true,
webSearch: normalizeWebSearchSettings(userSettings.webSearch),
h5Access: normalizeH5AccessSettings(h5AccessRes.settings),
h5AccessGeneratedToken: null,
isLoading: false,
error: null,
})
@ -92,6 +116,14 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
}
},
fetchH5Access: async () => {
const { settings } = await h5AccessApi.get()
set({
h5Access: normalizeH5AccessSettings(settings),
h5AccessGeneratedToken: null,
})
},
setPermissionMode: async (mode) => {
const prev = get().permissionMode
set({ permissionMode: mode })
@ -186,6 +218,41 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
set({ webSearch: prev })
}
},
enableH5Access: async () => {
const { settings, token } = await h5AccessApi.enable()
set({
h5Access: normalizeH5AccessSettings(settings),
h5AccessGeneratedToken: token,
})
},
disableH5Access: async () => {
const { settings } = await h5AccessApi.disable()
set({
h5Access: normalizeH5AccessSettings(settings),
h5AccessGeneratedToken: null,
})
},
regenerateH5AccessToken: async () => {
const { settings, token } = await h5AccessApi.regenerate()
set({
h5Access: normalizeH5AccessSettings(settings),
h5AccessGeneratedToken: token,
})
},
updateH5AccessSettings: async (input) => {
const { settings } = await h5AccessApi.update(input)
set({
h5Access: normalizeH5AccessSettings(settings),
})
},
clearH5AccessGeneratedToken: () => {
set({ h5AccessGeneratedToken: null })
},
}))
function normalizeWebSearchSettings(settings: WebSearchSettings | undefined): WebSearchSettings {
@ -195,3 +262,12 @@ function normalizeWebSearchSettings(settings: WebSearchSettings | undefined): We
braveApiKey: settings?.braveApiKey ?? '',
}
}
function normalizeH5AccessSettings(settings: H5AccessSettings | undefined): H5AccessSettings {
return {
enabled: settings?.enabled === true,
tokenPreview: settings?.tokenPreview ?? null,
allowedOrigins: Array.isArray(settings?.allowedOrigins) ? settings.allowedOrigins : [],
publicBaseUrl: settings?.publicBaseUrl ?? null,
}
}

View File

@ -12,6 +12,13 @@ export type WebSearchSettings = {
braveApiKey?: string
}
export type H5AccessSettings = {
enabled: boolean
tokenPreview: string | null
allowedOrigins: string[]
publicBaseUrl: string | null
}
export type ModelInfo = {
id: string
name: string