@fonlan/dsh-model-router 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/lib/client.js +197 -117
- package/lib/client.js.map +1 -1
- package/lib/index.js +20 -4
- package/lib/types/server/service.d.ts +17 -3
- package/package.json +4 -1
- package/src/client/index.tsx +37 -14
- package/src/client/locales.ts +8 -2
- package/src/client/route-switcher.tsx +31 -6
- package/src/client/{settings-section.css → settings-card.css} +91 -7
- package/src/client/{settings-section.tsx → settings-card.tsx} +138 -71
- package/src/server/service.ts +23 -4
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["useCallback","useSyncExternalStore","useState","useMemo","useSyncExternalStore","useState","useCallback","useMemo","Menu","IconGlobeOutline14","IconChevronDownOutline14"],"sources":["../../../src/client/locales.ts","../../../src/client/api.ts","../../../src/client/settings-section.tsx","../../../src/client/route-switcher.tsx","../../../src/client/index.tsx"],"sourcesContent":["// Locale namespace = the plugin's cordis id (kebab). DSH flat namespace keys\n// (settings + locale) use the internal id `model-router`; the scoped npm\n// package name is reserved for package-bound identifiers (exported `name`,\n// module-loader id, the /plugins/<pkg>/… route base).\nexport const LOCALE_NS = 'model-router'\n\nexport const zh = {\n settingsTitle: '模型路由',\n settingsDescription: '每个模型按 model id 聚合所有提供商;点击提供商切换该模型当前使用的路由,拖拽调整优先级顺序(供后续自动切换使用)。',\n loading: '加载中…',\n loadFailed: '加载失败:{message}',\n retry: '重试',\n refresh: '刷新',\n empty: '没有可路由的提供商或模型,请先在设置中添加提供商。',\n modelsCount: '共 {count} 个模型',\n activeBadge: '当前',\n noCredential: '未配置 API 密钥,不可选',\n dragHint: '拖拽调整优先级顺序',\n switchFailed: '切换失败:{message}',\n reorderFailed: '排序失败:{message}',\n providerCount: '{count} 个提供商',\n routeButtonLabel: '快速切换模型路由',\n routeButtonTitle: '切换当前模型的提供商',\n routeButtonAria: '切换当前模型提供商,当前为 {provider}',\n routeMenuTitle: '{model} · 选择提供商',\n routeLoading: '加载路由…',\n unknownProvider: '未知提供商',\n noCredentialShort: '未配置凭据',\n globalRouteHint: '切换后对所有会话的后续请求生效',\n showQuickSwitchLabel: '在输入栏显示快速路由切换按钮',\n showQuickSwitchDescription: '开启后,当当前模型是 model-router 路由时,在输入框模型选择器右侧显示 provider 快速切换按钮。',\n ignorePrefixLabel: '匹配时忽略模型 ID 前缀',\n ignorePrefixDescription: '开启后,deepseek/deepseek-v4-flash 与 deepseek-v4-flash 视为同一模型,统一显示不带前缀的模型 id(默认开启)。',\n}\n\nexport const en = {\n settingsTitle: 'Model Router',\n settingsDescription: 'Each model aggregates its providers by exact model id. Click a provider to switch where this model currently routes; drag to adjust the preference order (used by automatic failover later).',\n loading: 'Loading…',\n loadFailed: 'Load failed: {message}',\n retry: 'Retry',\n refresh: 'Refresh',\n empty: 'No routable providers or models yet — add providers in settings first.',\n modelsCount: '{count} models',\n activeBadge: 'active',\n noCredential: 'No API key configured — not selectable',\n dragHint: 'Drag to adjust preference order',\n switchFailed: 'Switch failed: {message}',\n reorderFailed: 'Reorder failed: {message}',\n providerCount: '{count} providers',\n routeButtonLabel: 'Quick model route switcher',\n routeButtonTitle: 'Switch the current model provider',\n routeButtonAria: 'Switch current model provider, currently {provider}',\n routeMenuTitle: '{model} · Choose provider',\n routeLoading: 'Loading routes…',\n unknownProvider: 'Unknown provider',\n noCredentialShort: 'No credentials',\n globalRouteHint: 'Changes apply to subsequent requests in all sessions',\n showQuickSwitchLabel: 'Show quick route switcher in the composer',\n showQuickSwitchDescription: 'When enabled, a provider quick-switch button appears next to the model selector when the current model is a model-router route.',\n ignorePrefixLabel: 'Ignore model ID prefix when matching',\n ignorePrefixDescription: 'When enabled, deepseek/deepseek-v4-flash and deepseek-v4-flash are treated as the same model and shown without the prefix (on by default).',\n}\n","/** Client transport for the model-router settings API. */\nexport interface RouterProviderView {\n id: string\n name: string\n credentialConfigured: boolean\n}\n\nexport interface RouterModelView {\n id: string\n name: string\n active: string | null\n order: string[]\n providers: RouterProviderView[]\n}\n\nexport interface ModelRouterState {\n /** Whether the composer quick route-switcher button is enabled. */\n showQuickSwitch: boolean\n /** Whether model ids are matched with their leading vendor/ prefix ignored. */\n ignoreModelIdPrefix: boolean\n providers: RouterProviderView[]\n models: RouterModelView[]\n}\n\nexport class ModelRouterApiError extends Error {\n constructor(readonly code: string, message: string) {\n super(message)\n }\n}\n\nasync function call<T>(method: string, payload: Record<string, unknown> = {}): Promise<T> {\n let response: Response\n try {\n response = await fetch(`/plugins/@fonlan/dsh-model-router/api/${method}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n })\n } catch (error) {\n throw new ModelRouterApiError('network', error instanceof Error ? error.message : String(error))\n }\n const parsed: { ok?: boolean; value?: unknown; error?: { code?: string; message?: string } } | null\n = await response.json().catch(() => null)\n if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) {\n throw new ModelRouterApiError(\n parsed?.error?.code ?? 'http',\n parsed?.error?.message ?? `HTTP ${response.status}`,\n )\n }\n return parsed.value as T\n}\n\nexport const api = {\n state: () => call<ModelRouterState>('state'),\n switchActive: (modelId: string, providerId: string) =>\n call<ModelRouterState>('switch-active', { modelId, providerId }),\n reorder: (modelId: string, order: string[]) =>\n call<ModelRouterState>('reorder', { modelId, order }),\n refresh: () => call<ModelRouterState>('refresh'),\n setShowQuickSwitch: (value: boolean) =>\n call<ModelRouterState>('set-show-quick-switch', { value }),\n setIgnoreModelIdPrefix: (value: boolean) =>\n call<ModelRouterState>('set-ignore-model-id-prefix', { value }),\n}\n","/**\n * The Model Router settings page: one card per merged model, provider chips\n * that switch the active route on click and reorder by native HTML5 drag &\n * drop (preference order for future automatic failover). All reads/mutations\n * go through the plugin's fenced API; the server persists through the\n * settings service, so a switch here is live for the next request globally.\n */\nimport { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react'\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { Translate } from '@deepseek-ai/dsh-client-ui-slots'\nimport { LOCALE_NS } from './locales'\nimport { api, type ModelRouterState, type RouterModelView } from './api'\nimport './settings-section.css'\n\nfunction arrayMove<T>(list: readonly T[], from: number, to: number): T[] {\n const next = [...list]\n const [item] = next.splice(from, 1)\n next.splice(to, 0, item)\n return next\n}\n\nfunction useLocaleRevision(ctx: ClientContext): number {\n const subscribe = useCallback(\n (onChange: () => void) => {\n try {\n return ctx.locale.subscribe(onChange)\n } catch {\n return () => {}\n }\n },\n [ctx],\n )\n const getSnapshot = useCallback(() => {\n try {\n return ctx.locale.getLocale().revision\n } catch {\n return 0\n }\n }, [ctx])\n return useSyncExternalStore(subscribe, getSnapshot)\n}\n\nexport function makeSettingsSection(ctx: ClientContext): () => JSX.Element {\n // Bound translation is namespace-typed; the section's props use the plain\n // Translate face (string keys), which the dict satisfies structurally.\n const t: Translate = (() => {\n try {\n return ctx.locale.bind(LOCALE_NS) as unknown as Translate\n } catch {\n return (key: string) => key\n }\n })()\n\n return function ModelRouterSettingsSection(): JSX.Element {\n useLocaleRevision(ctx)\n const [state, setState] = useState<ModelRouterState | null>(null)\n const [error, setError] = useState<string | null>(null)\n const [loading, setLoading] = useState(true)\n const [busyModel, setBusyModel] = useState<string | null>(null)\n const [drag, setDrag] = useState<{ modelId: string; index: number } | null>(null)\n const [quickSwitchBusy, setQuickSwitchBusy] = useState(false)\n const [ignorePrefixBusy, setIgnorePrefixBusy] = useState(false)\n\n const load = useCallback(async () => {\n setLoading(true)\n try {\n setState(await api.state())\n setError(null)\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setLoading(false)\n }\n }, [])\n\n useEffect(() => {\n void load()\n }, [load])\n\n const switchActive = useCallback(async (model: RouterModelView, providerId: string) => {\n if (model.active === providerId) return\n setBusyModel(model.id)\n try {\n setState(await api.switchActive(model.id, providerId))\n setError(null)\n } catch (cause) {\n setError(t('switchFailed', { message: cause instanceof Error ? cause.message : String(cause) }))\n } finally {\n setBusyModel(null)\n }\n }, [t])\n\n const reorder = useCallback(async (model: RouterModelView, from: number, to: number) => {\n if (from === to) return\n const order = arrayMove(model.order, from, to)\n setState(prev => prev === null ? prev : {\n ...prev,\n models: prev.models.map(m => (m.id === model.id ? { ...m, order } : m)),\n })\n setBusyModel(model.id)\n try {\n setState(await api.reorder(model.id, order))\n setError(null)\n } catch (cause) {\n setError(t('reorderFailed', { message: cause instanceof Error ? cause.message : String(cause) }))\n setState(prev => prev === null ? prev : {\n ...prev,\n models: prev.models.map(m => (m.id === model.id ? { ...m, order: model.order } : m)),\n })\n } finally {\n setBusyModel(null)\n }\n }, [t])\n\n const toggleQuickSwitch = useCallback(async (value: boolean) => {\n if (state === null) return\n const previous = state.showQuickSwitch\n setState(prev => prev === null ? prev : { ...prev, showQuickSwitch: value })\n setQuickSwitchBusy(true)\n try {\n setState(await api.setShowQuickSwitch(value))\n setError(null)\n } catch (cause) {\n setState(prev => prev === null ? prev : { ...prev, showQuickSwitch: previous })\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setQuickSwitchBusy(false)\n }\n }, [state])\n\n const toggleIgnorePrefix = useCallback(async (value: boolean) => {\n if (state === null) return\n const previous = state.ignoreModelIdPrefix\n setState(prev => prev === null ? prev : { ...prev, ignoreModelIdPrefix: value })\n setIgnorePrefixBusy(true)\n try {\n setState(await api.setIgnoreModelIdPrefix(value))\n setError(null)\n } catch (cause) {\n setState(prev => prev === null ? prev : { ...prev, ignoreModelIdPrefix: previous })\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setIgnorePrefixBusy(false)\n }\n }, [state])\n\n const models = useMemo(() => state?.models ?? [], [state])\n\n return (\n <div className=\"mr-root\">\n <p className=\"mr-description\">{t('settingsDescription')}</p>\n <label className=\"mr-quick-switch\">\n <input\n type=\"checkbox\"\n checked={state?.showQuickSwitch ?? true}\n disabled={loading || quickSwitchBusy}\n onChange={(event) => void toggleQuickSwitch(event.target.checked)}\n />\n <span className=\"mr-quick-switch-copy\">\n <span className=\"mr-quick-switch-label\">{t('showQuickSwitchLabel')}</span>\n <span className=\"mr-quick-switch-desc\">{t('showQuickSwitchDescription')}</span>\n </span>\n </label>\n <label className=\"mr-quick-switch\">\n <input\n type=\"checkbox\"\n checked={state?.ignoreModelIdPrefix ?? true}\n disabled={loading || ignorePrefixBusy}\n onChange={(event) => void toggleIgnorePrefix(event.target.checked)}\n />\n <span className=\"mr-quick-switch-copy\">\n <span className=\"mr-quick-switch-label\">{t('ignorePrefixLabel')}</span>\n <span className=\"mr-quick-switch-desc\">{t('ignorePrefixDescription')}</span>\n </span>\n </label>\n <div className=\"mr-toolbar\">\n <span className=\"mr-count\">{t('modelsCount', { count: models.length })}</span>\n <button type=\"button\" className=\"mr-button\" onClick={() => void load()} disabled={loading}>\n {t('refresh')}\n </button>\n </div>\n {error !== null && (\n <div className=\"mr-error\" role=\"alert\">{error}</div>\n )}\n {loading && models.length === 0 && (\n <div className=\"mr-empty\">{t('loading')}</div>\n )}\n {!loading && models.length === 0 && (\n <div className=\"mr-empty\">{t('empty')}</div>\n )}\n <div className=\"mr-models\">\n {models.map(model => (\n <ModelCard\n key={model.id}\n model={model}\n t={t}\n busy={busyModel === model.id}\n dragging={drag}\n onDragStart={index => setDrag({ modelId: model.id, index })}\n onDragEnd={() => setDrag(null)}\n onDrop={index => {\n if (drag !== null && drag.modelId === model.id) {\n void reorder(model, drag.index, index)\n }\n setDrag(null)\n }}\n onSwitch={providerId => void switchActive(model, providerId)}\n />\n ))}\n </div>\n </div>\n )\n }\n}\n\ninterface ModelCardProps {\n model: RouterModelView\n t: Translate\n busy: boolean\n dragging: { modelId: string; index: number } | null\n onDragStart: (index: number) => void\n onDragEnd: () => void\n onDrop: (index: number) => void\n onSwitch: (providerId: string) => void\n}\n\nfunction ModelCard(props: ModelCardProps): JSX.Element {\n const { model, t, busy, dragging } = props\n return (\n <section className={`mr-card${busy ? ' mr-card-busy' : ''}`}>\n <header className=\"mr-card-header\">\n <span className=\"mr-model-name\">{model.name}</span>\n <span className=\"mr-model-id\">{model.id}</span>\n <span className=\"mr-provider-count\">{t('providerCount', { count: model.providers.length })}</span>\n </header>\n <p className=\"mr-drag-hint\">{t('dragHint')}</p>\n <ul className=\"mr-providers\">\n {model.providers.map((provider, index) => {\n const active = provider.id === model.active\n const selectable = provider.credentialConfigured\n const dropTarget = dragging !== null && dragging.modelId === model.id && dragging.index !== index\n return (\n <li\n key={provider.id}\n className={[\n 'mr-provider',\n active ? 'mr-provider-active' : '',\n !selectable ? 'mr-provider-nocred' : '',\n dropTarget ? 'mr-provider-drop' : '',\n ].filter(Boolean).join(' ')}\n draggable={!busy && model.providers.length > 1}\n title={selectable ? undefined : t('noCredential')}\n onDragStart={(event) => {\n props.onDragStart(index)\n event.dataTransfer.effectAllowed = 'move'\n try {\n event.dataTransfer.setData('text/plain', model.id)\n } catch {\n // drag data is cosmetic for our own handler\n }\n }}\n onDragOver={(event) => {\n if (dragging !== null && dragging.modelId === model.id && dragging.index !== index) {\n event.preventDefault()\n event.dataTransfer.dropEffect = 'move'\n }\n }}\n onDrop={(event) => {\n if (dragging !== null && dragging.modelId === model.id) {\n event.preventDefault()\n props.onDrop(index)\n }\n }}\n onDragEnd={() => props.onDragEnd()}\n >\n <span className=\"mr-grip\" aria-hidden=\"true\">⠿</span>\n <button\n type=\"button\"\n className=\"mr-provider-button\"\n disabled={!selectable || busy || active}\n onClick={() => props.onSwitch(provider.id)}\n >\n <span className=\"mr-provider-name\">{provider.name}</span>\n {!selectable && <span className=\"mr-provider-nocred-label\">· {t('noCredential')}</span>}\n </button>\n {active && <span className=\"mr-active-badge\">{t('activeBadge')}</span>}\n </li>\n )\n })}\n </ul>\n </section>\n )\n}\n","import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react'\nimport type { ClientContext, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport { IconChevronDownOutline14, IconGlobeOutline14, Menu, type MenuEntry } from '@deepseek-ai/dsh-client-ui-primitives'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport { api, type ModelRouterState, type RouterModelView } from './api'\nimport { LOCALE_NS } from './locales'\nimport './route-switcher.css'\n\nconst ROUTER_PROVIDER_ID = 'model-router'\n\ninterface RouteSwitcherInjected {\n available: boolean\n directory: SnapshotStore<ModelDirectoryState>\n load: () => void\n}\n\ntype RouteSwitcherProps = PropsRuntime<'conversation.input.right'>\n & PropsLocale<typeof LOCALE_NS>\n & RouteSwitcherInjected\n\ntype RouteProvider = RouterModelView['providers'][number]\n\n/**\n * The quick route switcher lives beside DSH's model seat in the composer row.\n * It reads the same per-session model directory as the host model picker, but\n * only appears when the selected model is the virtual model-router route.\n */\nexport function RouteSwitcher({\n session,\n available,\n directory,\n load,\n t,\n}: RouteSwitcherProps): JSX.Element | null {\n const directoryState = useSyncExternalStore(\n onChange => directory.subscribe(onChange),\n () => directory.getSnapshot(),\n () => directory.getSnapshot(),\n )\n const current = directoryState.current\n const [routerState, setRouterState] = useState<ModelRouterState | null>(null)\n const [routerLoading, setRouterLoading] = useState(false)\n const [routerError, setRouterError] = useState<string | null>(null)\n const [open, setOpen] = useState(false)\n const [busyProvider, setBusyProvider] = useState<string | null>(null)\n\n const isRouterModel = available && current?.provider === ROUTER_PROVIDER_ID\n const modelId = isRouterModel ? current.model : null\n\n const loadRouterState = useCallback(async () => {\n setRouterLoading(true)\n try {\n setRouterState(await api.state())\n setRouterError(null)\n } catch (cause) {\n setRouterError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setRouterLoading(false)\n }\n }, [])\n\n useEffect(() => {\n if (!available) return\n load()\n }, [available, load])\n\n // Fetch the global toggle (and the model's provider list) on mount too, so a\n // \"hide quick switcher\" setting applies even before the model directory\n // settles. The button stays visible while the value is unknown (default on).\n useEffect(() => {\n if (!available) return\n void loadRouterState()\n }, [available])\n\n useEffect(() => {\n setOpen(false)\n setBusyProvider(null)\n setRouterState(null)\n setRouterError(null)\n }, [modelId])\n\n // The model-selection directory is refreshed when another session changes\n // the router settings document. Re-read the global router state after that\n // refresh so this button never shows a stale active provider. Include the\n // model id: switching between two router models can leave the directory in\n // the same ready state while changing the API row we need to display.\n useEffect(() => {\n if (!isRouterModel || modelId === null || directoryState.status !== 'ready') return\n void loadRouterState()\n }, [directoryState.status, isRouterModel, loadRouterState, modelId])\n\n const routeModel = useMemo(\n () => modelId === null\n ? undefined\n : routerState?.models.find(model => model.id === modelId),\n [modelId, routerState],\n )\n\n const providers = useMemo(() => {\n if (routeModel === undefined) return []\n const byId = new Map(routeModel.providers.map(provider => [provider.id, provider]))\n const ordered = routeModel.order\n .map(id => byId.get(id))\n .filter((provider): provider is RouteProvider => provider !== undefined)\n const seen = new Set(ordered.map(provider => provider.id))\n return [...ordered, ...routeModel.providers.filter(provider => !seen.has(provider.id))]\n }, [routeModel])\n\n const switchProvider = useCallback(async (providerId: string) => {\n if (modelId === null || routeModel === undefined || routeModel.active === providerId) {\n setOpen(false)\n return\n }\n setBusyProvider(providerId)\n try {\n const next = await api.switchActive(modelId, providerId)\n setRouterState(next)\n setRouterError(null)\n setOpen(false)\n // The model directory also listens for settings updates, but an explicit\n // refresh keeps the host model picker description in sync immediately.\n load()\n } catch (cause) {\n setRouterError(t('switchFailed', { message: cause instanceof Error ? cause.message : String(cause) }))\n } finally {\n setBusyProvider(null)\n }\n }, [load, modelId, routeModel, t])\n\n if (session.removed || !isRouterModel) return null\n if (routerState?.showQuickSwitch === false) return null\n\n const activeProvider = providers.find(provider => provider.id === routeModel?.active)\n const activeName = activeProvider?.name ?? routeModel?.active ?? t('routeLoading')\n if (routeModel !== undefined && providers.length < 2) return null\n\n const menuItems: MenuEntry[] = routeModel === undefined\n ? [{\n type: 'label',\n id: 'route-status',\n text: routerError ?? t('routeLoading'),\n }]\n : [\n {\n type: 'label',\n id: 'route-model',\n text: t('routeMenuTitle', { model: modelId }),\n },\n ...providers.map(provider => ({\n id: provider.id,\n label: (\n <span className=\"mr-route-item-copy\">\n <span className=\"mr-route-item-name\">{provider.name}</span>\n {!provider.credentialConfigured && (\n <span className=\"mr-route-item-note\">{t('noCredentialShort')}</span>\n )}\n </span>\n ),\n disabled: !provider.credentialConfigured || busyProvider !== null,\n })),\n ...(routerError === null ? [] : [{\n id: 'route-error',\n label: routerError,\n disabled: true,\n danger: true,\n }]),\n ]\n\n return (\n <Menu\n className=\"mr-route-anchor\"\n open={open}\n side=\"top\"\n align=\"end\"\n portal\n compact\n dense\n items={menuItems}\n selectedId={routeModel?.active ?? undefined}\n onSelect={id => {\n if (id === 'route-error' || routeModel === undefined) {\n void loadRouterState()\n load()\n return\n }\n void switchProvider(id)\n }}\n onClose={() => setOpen(false)}\n footer={[\n {\n type: 'label',\n id: 'route-global-hint',\n text: t('globalRouteHint'),\n },\n ]}\n anchor={(\n <button\n type=\"button\"\n className=\"mr-route-trigger\"\n aria-label={t('routeButtonAria', { provider: activeName })}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n disabled={routerLoading || busyProvider !== null}\n onClick={() => {\n setOpen(value => !value)\n if (!open) {\n void loadRouterState()\n load()\n }\n }}\n title={t('routeButtonTitle')}\n >\n <IconGlobeOutline14 />\n <span className=\"mr-route-trigger-label\">{activeName}</span>\n <span className=\"mr-route-trigger-chevron\" aria-hidden>\n <IconChevronDownOutline14 />\n </span>\n </button>\n )}\n />\n )\n}\n\n/** Register the quick route switcher into the composer right-side tool row. */\nexport function registerRouteSwitcher(ctx: ClientContext): void {\n const t = ctx.locale.bind(LOCALE_NS)\n ctx.inject(['slots', 'modelDirectories'], scope => {\n const models = scope.modelDirectories\n const sessions = scope.sessions\n scope.slots.inject('conversation.input.right', () => scope.slots.register({\n name: 'conversation.input.right',\n id: 'model-router-route-switcher',\n order: 100,\n label: () => t('routeButtonLabel'),\n locale: LOCALE_NS,\n inject: (sessionId) => {\n const directory = models.directoryFor(sessionId)\n const available = sessions.subagentAddress(sessionId) === undefined\n return {\n available,\n directory: directory.store,\n load: () => {\n if (available) directory.load().catch(() => {})\n },\n }\n },\n }, RouteSwitcher as never))\n })\n}","/**\n * @fonlan/dsh-model-router client half: the Model Router settings page plus\n * a quick provider switcher beside DSH's composer model selector.\n */\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport { LOCALE_NS, zh, en } from './locales'\nimport { makeSettingsSection } from './settings-section'\nimport { registerRouteSwitcher } from './route-switcher'\n\n/** Services required before mounting (provided by the client runtime). */\nexport const inject = ['slots', 'locale', 'modelDirectories', 'sessions']\n\n/** Client plugin body. */\nexport function apply(ctx: ClientContext): void {\n const t = ctx.locale.bind(LOCALE_NS)\n ctx.effect(() => {\n const off = ctx.locale.register(LOCALE_NS, { zh, en })\n return () => off()\n }, 'model-router: dictionaries')\n\n const SettingsSection = makeSettingsSection(ctx)\n ctx.slots.inject('settings.section', () =>\n ctx.slots.register({\n name: 'settings.section',\n id: 'model-router',\n order: 300,\n label: () => t('settingsTitle'),\n locale: LOCALE_NS,\n }, SettingsSection as never),\n )\n\n registerRouteSwitcher(ctx)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["useCallback","useSyncExternalStore","useState","useMemo","useSyncExternalStore","useState","useCallback","useRef","useMemo","Menu","IconGlobeOutline14","IconChevronDownOutline14"],"sources":["../../../src/client/locales.ts","../../../src/client/api.ts","../../../src/client/settings-card.tsx","../../../src/client/route-switcher.tsx","../../../src/client/index.tsx"],"sourcesContent":["// Locale namespace = the plugin's cordis id (kebab). DSH flat namespace keys\n// (settings + locale) use the internal id `model-router`; the scoped npm\n// package name is reserved for package-bound identifiers (exported `name`,\n// module-loader id, the /plugins/<pkg>/… route base).\nexport const LOCALE_NS = 'model-router'\n\nexport const zh = {\n settingsTitle: '模型路由',\n settingsCardDescription: '模型聚合、提供商切换与优先级顺序',\n expand: '展开',\n collapse: '收起',\n readOnly: '本部署的设置为只读。',\n loading: '加载中…',\n loadFailed: '加载失败:{message}',\n retry: '重试',\n refresh: '刷新',\n empty: '没有可路由的提供商或模型,请先在设置中添加提供商。',\n modelsCount: '共 {count} 个模型',\n activeBadge: '当前',\n noCredential: '未配置 API 密钥,不可选',\n dragHint: '拖拽调整优先级顺序',\n switchFailed: '切换失败:{message}',\n reorderFailed: '排序失败:{message}',\n providerCount: '{count} 个提供商',\n routeButtonLabel: '快速切换模型路由',\n routeButtonTitle: '切换当前模型的提供商',\n routeButtonAria: '切换当前模型提供商,当前为 {provider}',\n routeMenuTitle: '{model} · 选择提供商',\n routeLoading: '加载路由…',\n unknownProvider: '未知提供商',\n noCredentialShort: '未配置凭据',\n globalRouteHint: '切换后对所有会话的后续请求生效',\n showQuickSwitchLabel: '在输入栏显示快速路由切换按钮',\n showQuickSwitchDescription: '开启后,当当前模型是 model-router 路由时,在输入框模型选择器右侧显示 provider 快速切换按钮。',\n ignorePrefixLabel: '匹配时忽略模型 ID 前缀',\n ignorePrefixDescription: '开启后,deepseek/deepseek-v4-flash 与 deepseek-v4-flash 视为同一模型,统一显示不带前缀的模型 id(默认开启)。',\n}\n\nexport const en = {\n settingsTitle: 'Model Router',\n settingsCardDescription: 'Model aggregation, provider switching and preference order',\n expand: 'Expand',\n collapse: 'Collapse',\n readOnly: 'This deployment stores settings read-only.',\n loading: 'Loading…',\n loadFailed: 'Load failed: {message}',\n retry: 'Retry',\n refresh: 'Refresh',\n empty: 'No routable providers or models yet — add providers in settings first.',\n modelsCount: '{count} models',\n activeBadge: 'active',\n noCredential: 'No API key configured — not selectable',\n dragHint: 'Drag to adjust preference order',\n switchFailed: 'Switch failed: {message}',\n reorderFailed: 'Reorder failed: {message}',\n providerCount: '{count} providers',\n routeButtonLabel: 'Quick model route switcher',\n routeButtonTitle: 'Switch the current model provider',\n routeButtonAria: 'Switch current model provider, currently {provider}',\n routeMenuTitle: '{model} · Choose provider',\n routeLoading: 'Loading routes…',\n unknownProvider: 'Unknown provider',\n noCredentialShort: 'No credentials',\n globalRouteHint: 'Changes apply to subsequent requests in all sessions',\n showQuickSwitchLabel: 'Show quick route switcher in the composer',\n showQuickSwitchDescription: 'When enabled, a provider quick-switch button appears next to the model selector when the current model is a model-router route.',\n ignorePrefixLabel: 'Ignore model ID prefix when matching',\n ignorePrefixDescription: 'When enabled, deepseek/deepseek-v4-flash and deepseek-v4-flash are treated as the same model and shown without the prefix (on by default).',\n}\n","/** Client transport for the model-router settings API. */\nexport interface RouterProviderView {\n id: string\n name: string\n credentialConfigured: boolean\n}\n\nexport interface RouterModelView {\n id: string\n name: string\n active: string | null\n order: string[]\n providers: RouterProviderView[]\n}\n\nexport interface ModelRouterState {\n /** Whether the composer quick route-switcher button is enabled. */\n showQuickSwitch: boolean\n /** Whether model ids are matched with their leading vendor/ prefix ignored. */\n ignoreModelIdPrefix: boolean\n providers: RouterProviderView[]\n models: RouterModelView[]\n}\n\nexport class ModelRouterApiError extends Error {\n constructor(readonly code: string, message: string) {\n super(message)\n }\n}\n\nasync function call<T>(method: string, payload: Record<string, unknown> = {}): Promise<T> {\n let response: Response\n try {\n response = await fetch(`/plugins/@fonlan/dsh-model-router/api/${method}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n })\n } catch (error) {\n throw new ModelRouterApiError('network', error instanceof Error ? error.message : String(error))\n }\n const parsed: { ok?: boolean; value?: unknown; error?: { code?: string; message?: string } } | null\n = await response.json().catch(() => null)\n if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) {\n throw new ModelRouterApiError(\n parsed?.error?.code ?? 'http',\n parsed?.error?.message ?? `HTTP ${response.status}`,\n )\n }\n return parsed.value as T\n}\n\nexport const api = {\n state: () => call<ModelRouterState>('state'),\n switchActive: (modelId: string, providerId: string) =>\n call<ModelRouterState>('switch-active', { modelId, providerId }),\n reorder: (modelId: string, order: string[]) =>\n call<ModelRouterState>('reorder', { modelId, order }),\n refresh: () => call<ModelRouterState>('refresh'),\n setShowQuickSwitch: (value: boolean) =>\n call<ModelRouterState>('set-show-quick-switch', { value }),\n setIgnoreModelIdPrefix: (value: boolean) =>\n call<ModelRouterState>('set-ignore-model-id-prefix', { value }),\n}\n","/**\n * The Model Router Settings Card (设置 → 插件 → 插件配置 → 模型路由).\n *\n * Renders its own expandable chrome aligned with the built-in plugin cards\n * (external plugins cannot import PluginCard): a header naming the plugin and\n * what its settings govern, disclosing the router controls in place. All\n * reads/mutations go through the plugin's fenced API; the server persists\n * through the `model-router` settings namespace, so a switch here is live for\n * the next request globally.\n *\n * The bound settings scope (the same `model-router` namespace the host half\n * registers) supplies the card's dispatch state: while the namespace is merely\n * loading the card stays mounted, and when it is unavailable (deployment\n * without the host half) nothing renders — matching the built-in cards. A\n * read-only deployment shows the built-in card's banner and disables the\n * mutation controls.\n */\nimport { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react'\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { Translate } from '@deepseek-ai/dsh-client-ui-slots'\nimport { LOCALE_NS } from './locales'\nimport { api, type ModelRouterState, type RouterModelView } from './api'\nimport './settings-card.css'\n\n/** Client settings scope face (subset of @deepseek-ai/dsh-client-runtime). */\nexport interface SettingsScopeFace {\n getSnapshot(): {\n status: 'loading' | 'ready' | 'unavailable'\n writable: boolean\n }\n subscribe(listener: () => void): () => void\n}\n\nexport interface SettingsCardProps {\n /** The bound `model-router` settings scope (from the slot entry's inject face). */\n scope: SettingsScopeFace\n}\n\nfunction arrayMove<T>(list: readonly T[], from: number, to: number): T[] {\n const next = [...list]\n const [item] = next.splice(from, 1)\n next.splice(to, 0, item)\n return next\n}\n\nfunction useLocaleRevision(ctx: ClientContext): number {\n const subscribe = useCallback(\n (onChange: () => void) => {\n try {\n return ctx.locale.subscribe(onChange)\n } catch {\n return () => {}\n }\n },\n [ctx],\n )\n const getSnapshot = useCallback(() => {\n try {\n return ctx.locale.getLocale().revision\n } catch {\n return 0\n }\n }, [ctx])\n return useSyncExternalStore(subscribe, getSnapshot)\n}\n\nexport function makeSettingsCard(ctx: ClientContext): (props: SettingsCardProps) => JSX.Element | null {\n // Bound translation is namespace-typed; the card's props use the plain\n // Translate face (string keys), which the dict satisfies structurally.\n const t: Translate = (() => {\n try {\n return ctx.locale.bind(LOCALE_NS) as unknown as Translate\n } catch {\n return (key: string) => key\n }\n })()\n\n return function ModelRouterSettingsCard(props: SettingsCardProps): JSX.Element | null {\n const { scope } = props\n // Bind the methods: React invokes getSnapshot/subscribe as bare functions,\n // and SettingsScopeController's methods depend on `this`.\n const snapshot = useSyncExternalStore(\n (listener) => scope.subscribe(listener),\n () => scope.getSnapshot(),\n )\n useLocaleRevision(ctx)\n // Card-local disclosure: collapsed by default, like the built-in plugin cards.\n const [open, setOpen] = useState(false)\n const [state, setState] = useState<ModelRouterState | null>(null)\n const [error, setError] = useState<string | null>(null)\n const [loading, setLoading] = useState(true)\n const [busyModel, setBusyModel] = useState<string | null>(null)\n const [drag, setDrag] = useState<{ modelId: string; index: number } | null>(null)\n const [quickSwitchBusy, setQuickSwitchBusy] = useState(false)\n const [ignorePrefixBusy, setIgnorePrefixBusy] = useState(false)\n\n const load = useCallback(async () => {\n setLoading(true)\n try {\n setState(await api.state())\n setError(null)\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setLoading(false)\n }\n }, [])\n\n useEffect(() => {\n void load()\n }, [load])\n\n const switchActive = useCallback(async (model: RouterModelView, providerId: string) => {\n if (model.active === providerId) return\n setBusyModel(model.id)\n try {\n setState(await api.switchActive(model.id, providerId))\n setError(null)\n } catch (cause) {\n setError(t('switchFailed', { message: cause instanceof Error ? cause.message : String(cause) }))\n } finally {\n setBusyModel(null)\n }\n }, [t])\n\n const reorder = useCallback(async (model: RouterModelView, from: number, to: number) => {\n if (from === to) return\n const order = arrayMove(model.order, from, to)\n setState(prev => prev === null ? prev : {\n ...prev,\n models: prev.models.map(m => (m.id === model.id ? { ...m, order } : m)),\n })\n setBusyModel(model.id)\n try {\n setState(await api.reorder(model.id, order))\n setError(null)\n } catch (cause) {\n setError(t('reorderFailed', { message: cause instanceof Error ? cause.message : String(cause) }))\n setState(prev => prev === null ? prev : {\n ...prev,\n models: prev.models.map(m => (m.id === model.id ? { ...m, order: model.order } : m)),\n })\n } finally {\n setBusyModel(null)\n }\n }, [t])\n\n const toggleQuickSwitch = useCallback(async (value: boolean) => {\n if (state === null) return\n const previous = state.showQuickSwitch\n setState(prev => prev === null ? prev : { ...prev, showQuickSwitch: value })\n setQuickSwitchBusy(true)\n try {\n setState(await api.setShowQuickSwitch(value))\n setError(null)\n } catch (cause) {\n setState(prev => prev === null ? prev : { ...prev, showQuickSwitch: previous })\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setQuickSwitchBusy(false)\n }\n }, [state])\n\n const toggleIgnorePrefix = useCallback(async (value: boolean) => {\n if (state === null) return\n const previous = state.ignoreModelIdPrefix\n setState(prev => prev === null ? prev : { ...prev, ignoreModelIdPrefix: value })\n setIgnorePrefixBusy(true)\n try {\n setState(await api.setIgnoreModelIdPrefix(value))\n setError(null)\n } catch (cause) {\n setState(prev => prev === null ? prev : { ...prev, ignoreModelIdPrefix: previous })\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setIgnorePrefixBusy(false)\n }\n }, [state])\n\n // The namespace is served by the host once the scope is ready. While it is\n // merely loading, keep the card mounted (the tab already dispatched it);\n // if it is unavailable (deployment without the host half), render nothing.\n if (snapshot.status === 'unavailable') return null\n const writable = snapshot.writable\n\n const models = useMemo(() => state?.models ?? [], [state])\n\n return (\n <li className=\"mr-settings-card\" data-open={open ? '' : undefined}>\n <button\n type=\"button\"\n className=\"mr-settings-head\"\n aria-expanded={open}\n aria-label={(open ? t('collapse') : t('expand')) + ':' + t('settingsTitle')}\n onClick={() => setOpen(!open)}\n >\n <span className=\"mr-settings-head-text\">\n <span className=\"mr-settings-title\">{t('settingsTitle')}</span>\n <span className=\"mr-settings-sub\">{t('settingsCardDescription')}</span>\n </span>\n <span className=\"mr-settings-chevron\" data-open={open ? '' : undefined} aria-hidden=\"true\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\">\n <path d=\"M3.5 5.25L7 8.75L10.5 5.25\" stroke=\"currentColor\" strokeWidth=\"1.5\"\n strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n </span>\n </button>\n {open && (\n <div className=\"mr-settings-body\">\n {!writable && (\n <p className=\"mr-settings-readonly\" role=\"status\">{t('readOnly')}</p>\n )}\n <div className=\"mr-root\">\n <label className=\"mr-quick-switch\">\n <input\n type=\"checkbox\"\n checked={state?.showQuickSwitch ?? true}\n disabled={loading || quickSwitchBusy || !writable}\n onChange={(event) => void toggleQuickSwitch(event.target.checked)}\n />\n <span className=\"mr-quick-switch-copy\">\n <span className=\"mr-quick-switch-label\">{t('showQuickSwitchLabel')}</span>\n <span className=\"mr-quick-switch-desc\">{t('showQuickSwitchDescription')}</span>\n </span>\n </label>\n <label className=\"mr-quick-switch\">\n <input\n type=\"checkbox\"\n checked={state?.ignoreModelIdPrefix ?? true}\n disabled={loading || ignorePrefixBusy || !writable}\n onChange={(event) => void toggleIgnorePrefix(event.target.checked)}\n />\n <span className=\"mr-quick-switch-copy\">\n <span className=\"mr-quick-switch-label\">{t('ignorePrefixLabel')}</span>\n <span className=\"mr-quick-switch-desc\">{t('ignorePrefixDescription')}</span>\n </span>\n </label>\n <div className=\"mr-toolbar\">\n <span className=\"mr-count\">{t('modelsCount', { count: models.length })}</span>\n <button type=\"button\" className=\"mr-button\" onClick={() => void load()} disabled={loading}>\n {t('refresh')}\n </button>\n </div>\n {error !== null && (\n <div className=\"mr-error\" role=\"alert\">{error}</div>\n )}\n {loading && models.length === 0 && (\n <div className=\"mr-empty\">{t('loading')}</div>\n )}\n {!loading && models.length === 0 && (\n <div className=\"mr-empty\">{t('empty')}</div>\n )}\n <div className=\"mr-models\">\n {models.map(model => (\n <ModelCard\n key={model.id}\n model={model}\n t={t}\n readOnly={!writable}\n busy={busyModel === model.id}\n dragging={drag}\n onDragStart={index => setDrag({ modelId: model.id, index })}\n onDragEnd={() => setDrag(null)}\n onDrop={index => {\n if (drag !== null && drag.modelId === model.id) {\n void reorder(model, drag.index, index)\n }\n setDrag(null)\n }}\n onSwitch={providerId => void switchActive(model, providerId)}\n />\n ))}\n </div>\n </div>\n </div>\n )}\n </li>\n )\n }\n}\n\ninterface ModelCardProps {\n model: RouterModelView\n t: Translate\n readOnly: boolean\n busy: boolean\n dragging: { modelId: string; index: number } | null\n onDragStart: (index: number) => void\n onDragEnd: () => void\n onDrop: (index: number) => void\n onSwitch: (providerId: string) => void\n}\n\nfunction ModelCard(props: ModelCardProps): JSX.Element {\n const { model, t, readOnly, busy, dragging } = props\n return (\n <section className={`mr-card${busy ? ' mr-card-busy' : ''}`}>\n <header className=\"mr-card-header\">\n <span className=\"mr-model-name\">{model.name}</span>\n <span className=\"mr-model-id\">{model.id}</span>\n <span className=\"mr-provider-count\">{t('providerCount', { count: model.providers.length })}</span>\n </header>\n <p className=\"mr-drag-hint\">{t('dragHint')}</p>\n <ul className=\"mr-providers\">\n {model.providers.map((provider, index) => {\n const active = provider.id === model.active\n const selectable = provider.credentialConfigured\n const dropTarget = dragging !== null && dragging.modelId === model.id && dragging.index !== index\n return (\n <li\n key={provider.id}\n className={[\n 'mr-provider',\n active ? 'mr-provider-active' : '',\n !selectable ? 'mr-provider-nocred' : '',\n dropTarget ? 'mr-provider-drop' : '',\n ].filter(Boolean).join(' ')}\n draggable={!busy && !readOnly && model.providers.length > 1}\n title={selectable ? undefined : t('noCredential')}\n onDragStart={(event) => {\n props.onDragStart(index)\n event.dataTransfer.effectAllowed = 'move'\n try {\n event.dataTransfer.setData('text/plain', model.id)\n } catch {\n // drag data is cosmetic for our own handler\n }\n }}\n onDragOver={(event) => {\n if (dragging !== null && dragging.modelId === model.id && dragging.index !== index) {\n event.preventDefault()\n event.dataTransfer.dropEffect = 'move'\n }\n }}\n onDrop={(event) => {\n if (dragging !== null && dragging.modelId === model.id) {\n event.preventDefault()\n props.onDrop(index)\n }\n }}\n onDragEnd={() => props.onDragEnd()}\n >\n <span className=\"mr-grip\" aria-hidden=\"true\">⠿</span>\n <button\n type=\"button\"\n className=\"mr-provider-button\"\n disabled={!selectable || busy || active || readOnly}\n onClick={() => props.onSwitch(provider.id)}\n >\n <span className=\"mr-provider-name\">{provider.name}</span>\n {!selectable && <span className=\"mr-provider-nocred-label\">· {t('noCredential')}</span>}\n </button>\n {active && <span className=\"mr-active-badge\">{t('activeBadge')}</span>}\n </li>\n )\n })}\n </ul>\n </section>\n )\n}\n","import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'\nimport type { ClientContext, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport { IconChevronDownOutline14, IconGlobeOutline14, Menu, type MenuEntry } from '@deepseek-ai/dsh-client-ui-primitives'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport { api, type ModelRouterState, type RouterModelView } from './api'\nimport { LOCALE_NS } from './locales'\nimport './route-switcher.css'\n\nconst ROUTER_PROVIDER_ID = 'model-router'\n\ninterface RouteSwitcherInjected {\n available: boolean\n directory: SnapshotStore<ModelDirectoryState>\n load: () => void\n}\n\ntype RouteSwitcherProps = PropsRuntime<'conversation.input.right'>\n & PropsLocale<typeof LOCALE_NS>\n & RouteSwitcherInjected\n\ntype RouteProvider = RouterModelView['providers'][number]\n\n/**\n * The quick route switcher lives beside DSH's model seat in the composer row.\n * It reads the same per-session model directory as the host model picker, but\n * only appears when the selected model is the virtual model-router route.\n */\nexport function RouteSwitcher({\n session,\n available,\n directory,\n load,\n t,\n}: RouteSwitcherProps): JSX.Element | null {\n const directoryState = useSyncExternalStore(\n onChange => directory.subscribe(onChange),\n () => directory.getSnapshot(),\n () => directory.getSnapshot(),\n )\n const current = directoryState.current\n const [routerState, setRouterState] = useState<ModelRouterState | null>(null)\n const [routerLoading, setRouterLoading] = useState(false)\n const [routerError, setRouterError] = useState<string | null>(null)\n const [open, setOpen] = useState(false)\n const [busyProvider, setBusyProvider] = useState<string | null>(null)\n\n const isRouterModel = available && current?.provider === ROUTER_PROVIDER_ID\n const modelId = isRouterModel ? current.model : null\n\n /**\n * Fetch the router state. `silent` skips the busy state: background\n * refreshes (directory-ready, model switch) should never gray out the\n * button — only the initial load and explicit user actions do.\n */\n const loadRouterState = useCallback(async (silent = false) => {\n if (!silent) setRouterLoading(true)\n try {\n setRouterState(await api.state())\n setRouterError(null)\n } catch (cause) {\n setRouterError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n if (!silent) setRouterLoading(false)\n }\n }, [])\n\n useEffect(() => {\n if (!available) return\n load()\n }, [available, load])\n\n // Fetch the global toggle (and the model's provider list) on mount too, so a\n // \"hide quick switcher\" setting applies even before the model directory\n // settles. The button stays visible while the value is unknown (default on).\n useEffect(() => {\n if (!available) return\n void loadRouterState()\n }, [available])\n\n useEffect(() => {\n setOpen(false)\n setBusyProvider(null)\n // Keep the last-known router state across model switches: the global\n // provider count stays valid while the per-model row refetches, so a\n // single-provider router hides the button without a loading flash.\n setRouterError(null)\n }, [modelId])\n\n // The model-selection directory is refreshed when another session changes\n // the router settings document. Re-read the global router state after that\n // refresh so this button never shows a stale active provider. Include the\n // model id: switching between two router models can leave the directory in\n // the same ready state while changing the API row we need to display.\n //\n // This refresh is debounced and silent: directory refreshes fire in bursts\n // (each settings change re-loads it), and the whole point of the silent\n // mode is that background refreshes never gray out the button.\n const refreshTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n useEffect(() => {\n if (!isRouterModel || modelId === null || directoryState.status !== 'ready') return\n if (refreshTimer.current !== null) clearTimeout(refreshTimer.current)\n refreshTimer.current = setTimeout(() => {\n refreshTimer.current = null\n void loadRouterState(true)\n }, 300)\n return () => {\n if (refreshTimer.current !== null) clearTimeout(refreshTimer.current)\n refreshTimer.current = null\n }\n }, [directoryState.status, isRouterModel, loadRouterState, modelId])\n\n const routeModel = useMemo(\n () => modelId === null\n ? undefined\n : routerState?.models.find(model => model.id === modelId),\n [modelId, routerState],\n )\n\n const providers = useMemo(() => {\n if (routeModel === undefined) return []\n const byId = new Map(routeModel.providers.map(provider => [provider.id, provider]))\n const ordered = routeModel.order\n .map(id => byId.get(id))\n .filter((provider): provider is RouteProvider => provider !== undefined)\n const seen = new Set(ordered.map(provider => provider.id))\n return [...ordered, ...routeModel.providers.filter(provider => !seen.has(provider.id))]\n }, [routeModel])\n\n const switchProvider = useCallback(async (providerId: string) => {\n if (modelId === null || routeModel === undefined || routeModel.active === providerId) {\n setOpen(false)\n return\n }\n setBusyProvider(providerId)\n try {\n const next = await api.switchActive(modelId, providerId)\n setRouterState(next)\n setRouterError(null)\n setOpen(false)\n // The model directory also listens for settings updates, but an explicit\n // refresh keeps the host model picker description in sync immediately.\n load()\n } catch (cause) {\n setRouterError(t('switchFailed', { message: cause instanceof Error ? cause.message : String(cause) }))\n } finally {\n setBusyProvider(null)\n }\n }, [load, modelId, routeModel, t])\n\n if (session.removed || !isRouterModel) return null\n if (routerState?.showQuickSwitch === false) return null\n\n const activeProvider = providers.find(provider => provider.id === routeModel?.active)\n const activeName = activeProvider?.name ?? routeModel?.active ?? t('routeLoading')\n // Nothing to switch between: hide when the router serves fewer than two\n // providers in total (known as soon as the global state resolves, even\n // while the per-model row is still loading), or when the current model\n // is served by a single provider.\n if (routerState !== null && routerState.providers.length < 2) return null\n if (routeModel !== undefined && providers.length < 2) return null\n\n const menuItems: MenuEntry[] = routeModel === undefined\n ? [{\n type: 'label',\n id: 'route-status',\n text: routerError ?? t('routeLoading'),\n }]\n : [\n {\n type: 'label',\n id: 'route-model',\n text: t('routeMenuTitle', { model: modelId }),\n },\n ...providers.map(provider => ({\n id: provider.id,\n label: (\n <span className=\"mr-route-item-copy\">\n <span className=\"mr-route-item-name\">{provider.name}</span>\n {!provider.credentialConfigured && (\n <span className=\"mr-route-item-note\">{t('noCredentialShort')}</span>\n )}\n </span>\n ),\n disabled: !provider.credentialConfigured || busyProvider !== null,\n })),\n ...(routerError === null ? [] : [{\n id: 'route-error',\n label: routerError,\n disabled: true,\n danger: true,\n }]),\n ]\n\n return (\n <Menu\n className=\"mr-route-anchor\"\n open={open}\n side=\"top\"\n align=\"end\"\n portal\n compact\n dense\n items={menuItems}\n selectedId={routeModel?.active ?? undefined}\n onSelect={id => {\n if (id === 'route-error' || routeModel === undefined) {\n void loadRouterState()\n load()\n return\n }\n void switchProvider(id)\n }}\n onClose={() => setOpen(false)}\n footer={[\n {\n type: 'label',\n id: 'route-global-hint',\n text: t('globalRouteHint'),\n },\n ]}\n anchor={(\n <button\n type=\"button\"\n className=\"mr-route-trigger\"\n aria-label={t('routeButtonAria', { provider: activeName })}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n disabled={routerLoading || busyProvider !== null}\n onClick={() => {\n setOpen(value => !value)\n if (!open) {\n void loadRouterState()\n load()\n }\n }}\n title={t('routeButtonTitle')}\n >\n <IconGlobeOutline14 />\n <span className=\"mr-route-trigger-label\">{activeName}</span>\n <span className=\"mr-route-trigger-chevron\" aria-hidden>\n <IconChevronDownOutline14 />\n </span>\n </button>\n )}\n />\n )\n}\n\n/** Register the quick route switcher into the composer right-side tool row. */\nexport function registerRouteSwitcher(ctx: ClientContext): void {\n const t = ctx.locale.bind(LOCALE_NS)\n ctx.inject(['slots', 'modelDirectories'], scope => {\n const models = scope.modelDirectories\n const sessions = scope.sessions\n scope.slots.inject('conversation.input.right', () => scope.slots.register({\n name: 'conversation.input.right',\n id: 'model-router-route-switcher',\n order: 100,\n label: () => t('routeButtonLabel'),\n locale: LOCALE_NS,\n inject: (sessionId) => {\n const directory = models.directoryFor(sessionId)\n const available = sessions.subagentAddress(sessionId) === undefined\n return {\n available,\n directory: directory.store,\n load: () => {\n if (available) directory.load().catch(() => {})\n },\n }\n },\n }, RouteSwitcher as never))\n })\n}","/**\n * @fonlan/dsh-model-router client half: the plugin's own Settings Card\n * (设置 → 插件 → 插件配置 → 模型路由) plus a quick provider switcher beside\n * DSH's composer model selector.\n */\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport { LOCALE_NS, zh, en } from './locales'\nimport { makeSettingsCard, type SettingsScopeFace } from './settings-card'\nimport { registerRouteSwitcher } from './route-switcher'\n\n/** The settings namespace this card edits (must match the host half). */\nconst ROUTER_NS = 'model-router'\n\n/** Slots face: the keyed `settings.plugin.item` declaration ships with the web\n * app's ui-settings-plugins package, which external plugins do not depend on;\n * the register call below is typed through this local face (erased at build). */\ninterface Slots {\n inject(name: string, callback: () => unknown): unknown\n register(def: { name: string; key: string; inject?: () => unknown }, component: unknown): unknown\n}\n\n/** Services required before mounting (provided by the client runtime). */\nexport const inject = ['slots', 'locale', 'modelDirectories', 'sessions', 'settingsScope', 'connection', 'remote']\n\n/** Client plugin body. */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => {\n const off = ctx.locale.register(LOCALE_NS, { zh, en })\n return () => off()\n }, 'model-router: dictionaries')\n\n const SettingsCard = makeSettingsCard(ctx)\n\n // The card registers on the `settings.plugin.item` slot keyed by the\n // `model-router` settings namespace (the same string the host half\n // registers), so it appears inside 设置 → 插件 → 插件配置, paired with the\n // namespace by the tab.\n const services = ctx as unknown as {\n slots: Slots\n settingsScope: { bind(spec: { namespace: string }): SettingsScopeFace }\n }\n const scope = services.settingsScope.bind({ namespace: ROUTER_NS })\n\n services.slots.inject('settings.plugin.item', () =>\n services.slots.register(\n {\n name: 'settings.plugin.item',\n key: ROUTER_NS,\n inject: () => ({ scope }),\n },\n SettingsCard,\n ),\n )\n\n registerRouteSwitcher(ctx)\n}\n"],"mappings":"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"}
|
package/lib/index.js
CHANGED
|
@@ -496,8 +496,11 @@ var ModelRouterService = class {
|
|
|
496
496
|
credentialConfigured: /* @__PURE__ */ new Map()
|
|
497
497
|
};
|
|
498
498
|
inflight = null;
|
|
499
|
+
cachedAt = 0;
|
|
499
500
|
scope;
|
|
500
501
|
registration;
|
|
502
|
+
/** How long a built catalog is considered fresh before being rebuilt. */
|
|
503
|
+
catalogTtlMs = 3e4;
|
|
501
504
|
constructor(ctx) {
|
|
502
505
|
this.ctx = ctx;
|
|
503
506
|
}
|
|
@@ -537,11 +540,24 @@ var ModelRouterService = class {
|
|
|
537
540
|
llm() {
|
|
538
541
|
return this.ctx.llm;
|
|
539
542
|
}
|
|
540
|
-
/**
|
|
541
|
-
|
|
543
|
+
/**
|
|
544
|
+
* Return the live catalog, rebuilding it only when it is stale (older than
|
|
545
|
+
* `catalogTtlMs`) or `force` is set. Concurrent callers share one in-flight
|
|
546
|
+
* build; a failed build keeps the last good catalog.
|
|
547
|
+
*
|
|
548
|
+
* The TTL is what keeps the composer quick-switcher snappy: every
|
|
549
|
+
* `state()` RPC goes through here, and without a cache each one rebuilt the
|
|
550
|
+
* catalog — including per-provider `listModels` network calls — which is
|
|
551
|
+
* what made the button gray out (routerLoading) for the whole duration.
|
|
552
|
+
* Topology events (`llm/adapters-updated`) and explicit user refreshes pass
|
|
553
|
+
* `force` so the cache never hides real changes.
|
|
554
|
+
*/
|
|
555
|
+
refreshCatalog(force = false) {
|
|
556
|
+
if (!force && this.inflight === null && Date.now() - this.cachedAt < this.catalogTtlMs) return Promise.resolve(this.cached);
|
|
542
557
|
if (this.inflight !== null) return this.inflight;
|
|
543
558
|
this.inflight = buildCatalog(this.ctx, this.config().ignoreModelIdPrefix).then((catalog) => {
|
|
544
559
|
this.cached = catalog;
|
|
560
|
+
this.cachedAt = Date.now();
|
|
545
561
|
return catalog;
|
|
546
562
|
}).catch((error) => {
|
|
547
563
|
this.ctx.logger.warn(`model-router: catalog refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -551,9 +567,9 @@ var ModelRouterService = class {
|
|
|
551
567
|
});
|
|
552
568
|
return this.inflight;
|
|
553
569
|
}
|
|
554
|
-
/** Refresh, then persist any config drift (migration, new/vanished models). */
|
|
570
|
+
/** Refresh (forced), then persist any config drift (migration, new/vanished models). */
|
|
555
571
|
async refreshAndReconcile() {
|
|
556
|
-
const catalog = await this.refreshCatalog();
|
|
572
|
+
const catalog = await this.refreshCatalog(true);
|
|
557
573
|
if (this.scope === void 0) return;
|
|
558
574
|
const current = this.config();
|
|
559
575
|
const { models, changed } = reconcileConfig(current, catalog.models);
|
|
@@ -41,8 +41,11 @@ export declare class ModelRouterService implements RouterFacts {
|
|
|
41
41
|
private readonly ctx;
|
|
42
42
|
private cached;
|
|
43
43
|
private inflight;
|
|
44
|
+
private cachedAt;
|
|
44
45
|
private scope;
|
|
45
46
|
private registration;
|
|
47
|
+
/** How long a built catalog is considered fresh before being rebuilt. */
|
|
48
|
+
private readonly catalogTtlMs;
|
|
46
49
|
constructor(ctx: Context);
|
|
47
50
|
/** Register settings + adapter and wire topology listeners. */
|
|
48
51
|
start(): void;
|
|
@@ -54,9 +57,20 @@ export declare class ModelRouterService implements RouterFacts {
|
|
|
54
57
|
config(): RouterConfigShape;
|
|
55
58
|
providerNames(): ReadonlyMap<string, string>;
|
|
56
59
|
llm(): import("@deepseek-ai/dsh-llm").LlmRuntime;
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Return the live catalog, rebuilding it only when it is stale (older than
|
|
62
|
+
* `catalogTtlMs`) or `force` is set. Concurrent callers share one in-flight
|
|
63
|
+
* build; a failed build keeps the last good catalog.
|
|
64
|
+
*
|
|
65
|
+
* The TTL is what keeps the composer quick-switcher snappy: every
|
|
66
|
+
* `state()` RPC goes through here, and without a cache each one rebuilt the
|
|
67
|
+
* catalog — including per-provider `listModels` network calls — which is
|
|
68
|
+
* what made the button gray out (routerLoading) for the whole duration.
|
|
69
|
+
* Topology events (`llm/adapters-updated`) and explicit user refreshes pass
|
|
70
|
+
* `force` so the cache never hides real changes.
|
|
71
|
+
*/
|
|
72
|
+
refreshCatalog(force?: boolean): Promise<RouterCatalog>;
|
|
73
|
+
/** Refresh (forced), then persist any config drift (migration, new/vanished models). */
|
|
60
74
|
refreshAndReconcile(): Promise<void>;
|
|
61
75
|
/** Effective active provider for one model id (null when unroutable). */
|
|
62
76
|
activeFor(modelId: string): string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonlan/dsh-model-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "DSH plugin: a virtual model-router provider that aggregates every configured provider and model, routes each request to the model's currently active provider, and ships a settings page for per-model provider ordering and switching.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "rm -rf lib && tsc -p tsconfig.build.json && tsdown",
|
|
48
|
+
"prepare": "pnpm build",
|
|
48
49
|
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.client.json",
|
|
49
50
|
"bundle": "tsdown",
|
|
50
51
|
"watch": "tsdown --watch",
|
|
@@ -62,6 +63,8 @@
|
|
|
62
63
|
"@deepseek-ai/dsh-client-ui-slots",
|
|
63
64
|
"@deepseek-ai/dsh-client-ui-primitives",
|
|
64
65
|
"@deepseek-ai/dsh-client-ui-settings",
|
|
66
|
+
"@deepseek-ai/dsh-client-connection",
|
|
67
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
65
68
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
66
69
|
"@deepseek-ai/dsh-client-ui-model-selection"
|
|
67
70
|
]
|
package/src/client/index.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fonlan/dsh-model-router client half: the
|
|
3
|
-
* a quick provider switcher beside
|
|
2
|
+
* @fonlan/dsh-model-router client half: the plugin's own Settings Card
|
|
3
|
+
* (设置 → 插件 → 插件配置 → 模型路由) plus a quick provider switcher beside
|
|
4
|
+
* DSH's composer model selector.
|
|
4
5
|
*/
|
|
5
6
|
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
6
7
|
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
@@ -9,29 +10,51 @@ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
|
9
10
|
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
10
11
|
import type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'
|
|
11
12
|
import { LOCALE_NS, zh, en } from './locales'
|
|
12
|
-
import {
|
|
13
|
+
import { makeSettingsCard, type SettingsScopeFace } from './settings-card'
|
|
13
14
|
import { registerRouteSwitcher } from './route-switcher'
|
|
14
15
|
|
|
16
|
+
/** The settings namespace this card edits (must match the host half). */
|
|
17
|
+
const ROUTER_NS = 'model-router'
|
|
18
|
+
|
|
19
|
+
/** Slots face: the keyed `settings.plugin.item` declaration ships with the web
|
|
20
|
+
* app's ui-settings-plugins package, which external plugins do not depend on;
|
|
21
|
+
* the register call below is typed through this local face (erased at build). */
|
|
22
|
+
interface Slots {
|
|
23
|
+
inject(name: string, callback: () => unknown): unknown
|
|
24
|
+
register(def: { name: string; key: string; inject?: () => unknown }, component: unknown): unknown
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
/** Services required before mounting (provided by the client runtime). */
|
|
16
|
-
export const inject = ['slots', 'locale', 'modelDirectories', 'sessions']
|
|
28
|
+
export const inject = ['slots', 'locale', 'modelDirectories', 'sessions', 'settingsScope', 'connection', 'remote']
|
|
17
29
|
|
|
18
30
|
/** Client plugin body. */
|
|
19
31
|
export function apply(ctx: ClientContext): void {
|
|
20
|
-
const t = ctx.locale.bind(LOCALE_NS)
|
|
21
32
|
ctx.effect(() => {
|
|
22
33
|
const off = ctx.locale.register(LOCALE_NS, { zh, en })
|
|
23
34
|
return () => off()
|
|
24
35
|
}, 'model-router: dictionaries')
|
|
25
36
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
const SettingsCard = makeSettingsCard(ctx)
|
|
38
|
+
|
|
39
|
+
// The card registers on the `settings.plugin.item` slot keyed by the
|
|
40
|
+
// `model-router` settings namespace (the same string the host half
|
|
41
|
+
// registers), so it appears inside 设置 → 插件 → 插件配置, paired with the
|
|
42
|
+
// namespace by the tab.
|
|
43
|
+
const services = ctx as unknown as {
|
|
44
|
+
slots: Slots
|
|
45
|
+
settingsScope: { bind(spec: { namespace: string }): SettingsScopeFace }
|
|
46
|
+
}
|
|
47
|
+
const scope = services.settingsScope.bind({ namespace: ROUTER_NS })
|
|
48
|
+
|
|
49
|
+
services.slots.inject('settings.plugin.item', () =>
|
|
50
|
+
services.slots.register(
|
|
51
|
+
{
|
|
52
|
+
name: 'settings.plugin.item',
|
|
53
|
+
key: ROUTER_NS,
|
|
54
|
+
inject: () => ({ scope }),
|
|
55
|
+
},
|
|
56
|
+
SettingsCard,
|
|
57
|
+
),
|
|
35
58
|
)
|
|
36
59
|
|
|
37
60
|
registerRouteSwitcher(ctx)
|
package/src/client/locales.ts
CHANGED
|
@@ -6,7 +6,10 @@ export const LOCALE_NS = 'model-router'
|
|
|
6
6
|
|
|
7
7
|
export const zh = {
|
|
8
8
|
settingsTitle: '模型路由',
|
|
9
|
-
|
|
9
|
+
settingsCardDescription: '模型聚合、提供商切换与优先级顺序',
|
|
10
|
+
expand: '展开',
|
|
11
|
+
collapse: '收起',
|
|
12
|
+
readOnly: '本部署的设置为只读。',
|
|
10
13
|
loading: '加载中…',
|
|
11
14
|
loadFailed: '加载失败:{message}',
|
|
12
15
|
retry: '重试',
|
|
@@ -35,7 +38,10 @@ export const zh = {
|
|
|
35
38
|
|
|
36
39
|
export const en = {
|
|
37
40
|
settingsTitle: 'Model Router',
|
|
38
|
-
|
|
41
|
+
settingsCardDescription: 'Model aggregation, provider switching and preference order',
|
|
42
|
+
expand: 'Expand',
|
|
43
|
+
collapse: 'Collapse',
|
|
44
|
+
readOnly: 'This deployment stores settings read-only.',
|
|
39
45
|
loading: 'Loading…',
|
|
40
46
|
loadFailed: 'Load failed: {message}',
|
|
41
47
|
retry: 'Retry',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
|
|
2
2
|
import type { ClientContext, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
|
3
3
|
import type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client'
|
|
4
4
|
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
@@ -50,15 +50,20 @@ export function RouteSwitcher({
|
|
|
50
50
|
const isRouterModel = available && current?.provider === ROUTER_PROVIDER_ID
|
|
51
51
|
const modelId = isRouterModel ? current.model : null
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Fetch the router state. `silent` skips the busy state: background
|
|
55
|
+
* refreshes (directory-ready, model switch) should never gray out the
|
|
56
|
+
* button — only the initial load and explicit user actions do.
|
|
57
|
+
*/
|
|
58
|
+
const loadRouterState = useCallback(async (silent = false) => {
|
|
59
|
+
if (!silent) setRouterLoading(true)
|
|
55
60
|
try {
|
|
56
61
|
setRouterState(await api.state())
|
|
57
62
|
setRouterError(null)
|
|
58
63
|
} catch (cause) {
|
|
59
64
|
setRouterError(cause instanceof Error ? cause.message : String(cause))
|
|
60
65
|
} finally {
|
|
61
|
-
setRouterLoading(false)
|
|
66
|
+
if (!silent) setRouterLoading(false)
|
|
62
67
|
}
|
|
63
68
|
}, [])
|
|
64
69
|
|
|
@@ -78,7 +83,9 @@ export function RouteSwitcher({
|
|
|
78
83
|
useEffect(() => {
|
|
79
84
|
setOpen(false)
|
|
80
85
|
setBusyProvider(null)
|
|
81
|
-
|
|
86
|
+
// Keep the last-known router state across model switches: the global
|
|
87
|
+
// provider count stays valid while the per-model row refetches, so a
|
|
88
|
+
// single-provider router hides the button without a loading flash.
|
|
82
89
|
setRouterError(null)
|
|
83
90
|
}, [modelId])
|
|
84
91
|
|
|
@@ -87,9 +94,22 @@ export function RouteSwitcher({
|
|
|
87
94
|
// refresh so this button never shows a stale active provider. Include the
|
|
88
95
|
// model id: switching between two router models can leave the directory in
|
|
89
96
|
// the same ready state while changing the API row we need to display.
|
|
97
|
+
//
|
|
98
|
+
// This refresh is debounced and silent: directory refreshes fire in bursts
|
|
99
|
+
// (each settings change re-loads it), and the whole point of the silent
|
|
100
|
+
// mode is that background refreshes never gray out the button.
|
|
101
|
+
const refreshTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
90
102
|
useEffect(() => {
|
|
91
103
|
if (!isRouterModel || modelId === null || directoryState.status !== 'ready') return
|
|
92
|
-
|
|
104
|
+
if (refreshTimer.current !== null) clearTimeout(refreshTimer.current)
|
|
105
|
+
refreshTimer.current = setTimeout(() => {
|
|
106
|
+
refreshTimer.current = null
|
|
107
|
+
void loadRouterState(true)
|
|
108
|
+
}, 300)
|
|
109
|
+
return () => {
|
|
110
|
+
if (refreshTimer.current !== null) clearTimeout(refreshTimer.current)
|
|
111
|
+
refreshTimer.current = null
|
|
112
|
+
}
|
|
93
113
|
}, [directoryState.status, isRouterModel, loadRouterState, modelId])
|
|
94
114
|
|
|
95
115
|
const routeModel = useMemo(
|
|
@@ -135,6 +155,11 @@ export function RouteSwitcher({
|
|
|
135
155
|
|
|
136
156
|
const activeProvider = providers.find(provider => provider.id === routeModel?.active)
|
|
137
157
|
const activeName = activeProvider?.name ?? routeModel?.active ?? t('routeLoading')
|
|
158
|
+
// Nothing to switch between: hide when the router serves fewer than two
|
|
159
|
+
// providers in total (known as soon as the global state resolves, even
|
|
160
|
+
// while the per-model row is still loading), or when the current model
|
|
161
|
+
// is served by a single provider.
|
|
162
|
+
if (routerState !== null && routerState.providers.length < 2) return null
|
|
138
163
|
if (routeModel !== undefined && providers.length < 2) return null
|
|
139
164
|
|
|
140
165
|
const menuItems: MenuEntry[] = routeModel === undefined
|
|
@@ -1,16 +1,100 @@
|
|
|
1
|
-
/* Model Router settings
|
|
2
|
-
|
|
1
|
+
/* Model Router settings card — chrome aligned with the built-in plugin cards
|
|
2
|
+
(设置 → 插件 → 插件配置), theme-consistent via DSW alias variables. */
|
|
3
|
+
.mr-settings-card {
|
|
4
|
+
border: 1px solid var(--dsw-alias-border-l2, #2a3346);
|
|
5
|
+
background: var(--dsw-alias-bg-layer-3, #0d1017);
|
|
6
|
+
border-radius: 12px;
|
|
7
|
+
list-style: none;
|
|
8
|
+
transition: border-color .16s, background .16s;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.mr-settings-card:hover {
|
|
12
|
+
border-color: var(--dsw-alias-label-dimmed, #4a5468);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.mr-settings-card[data-open] {
|
|
16
|
+
background: var(--dsw-alias-bg-layer-2, #141a26);
|
|
17
|
+
border-color: var(--dsw-alias-label-dimmed, #4a5468);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.mr-settings-head {
|
|
21
|
+
appearance: none;
|
|
22
|
+
width: 100%;
|
|
23
|
+
font: inherit;
|
|
24
|
+
color: inherit;
|
|
25
|
+
text-align: left;
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
background: transparent;
|
|
28
|
+
border: 0;
|
|
29
|
+
border-radius: 12px;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 12px;
|
|
32
|
+
padding: 14px 16px;
|
|
33
|
+
display: flex;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.mr-settings-head:focus-visible {
|
|
37
|
+
outline: 2px solid var(--dsw-alias-brand-primary, #4f8cff);
|
|
38
|
+
outline-offset: -2px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.mr-settings-head-text {
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
flex: 1;
|
|
44
|
+
gap: 4px;
|
|
45
|
+
min-width: 0;
|
|
46
|
+
display: flex;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.mr-settings-title {
|
|
50
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
51
|
+
font-size: 15px;
|
|
52
|
+
font-weight: 600;
|
|
53
|
+
line-height: 1.4;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.mr-settings-sub {
|
|
57
|
+
color: var(--dsw-alias-label-tertiary, #8b93a7);
|
|
58
|
+
font-size: 13px;
|
|
59
|
+
line-height: 1.5;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.mr-settings-chevron {
|
|
63
|
+
color: var(--dsw-alias-label-tertiary, #8b93a7);
|
|
64
|
+
flex: none;
|
|
65
|
+
transition: transform .16s;
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
justify-content: center;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.mr-settings-chevron[data-open] {
|
|
72
|
+
transform: rotate(180deg);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.mr-settings-body {
|
|
76
|
+
border-top: 1px solid var(--dsw-alias-border-l2, #2a3346);
|
|
77
|
+
margin: 0 16px;
|
|
78
|
+
padding: 12px 0 8px;
|
|
3
79
|
display: flex;
|
|
4
80
|
flex-direction: column;
|
|
5
81
|
gap: 12px;
|
|
6
|
-
padding: 4px 0 24px;
|
|
7
82
|
}
|
|
8
83
|
|
|
9
|
-
.mr-
|
|
84
|
+
.mr-settings-readonly {
|
|
85
|
+
color: var(--dsw-alias-label-tertiary, #8b93a7);
|
|
10
86
|
margin: 0;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
87
|
+
font-size: 12px;
|
|
88
|
+
line-height: 1.5;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* ── card body: the router controls ─────────────────────────────────────── */
|
|
92
|
+
|
|
93
|
+
.mr-root {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
gap: 12px;
|
|
97
|
+
padding: 0 0 4px;
|
|
14
98
|
}
|
|
15
99
|
|
|
16
100
|
.mr-quick-switch {
|