@fonlan/dsh-model-router 0.1.0 → 0.2.0

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.
@@ -58,6 +58,7 @@ export function makeSettingsSection(ctx: ClientContext): () => JSX.Element {
58
58
  const [loading, setLoading] = useState(true)
59
59
  const [busyModel, setBusyModel] = useState<string | null>(null)
60
60
  const [drag, setDrag] = useState<{ modelId: string; index: number } | null>(null)
61
+ const [quickSwitchBusy, setQuickSwitchBusy] = useState(false)
61
62
 
62
63
  const load = useCallback(async () => {
63
64
  setLoading(true)
@@ -110,11 +111,39 @@ export function makeSettingsSection(ctx: ClientContext): () => JSX.Element {
110
111
  }
111
112
  }, [t])
112
113
 
114
+ const toggleQuickSwitch = useCallback(async (value: boolean) => {
115
+ if (state === null) return
116
+ const previous = state.showQuickSwitch
117
+ setState(prev => prev === null ? prev : { ...prev, showQuickSwitch: value })
118
+ setQuickSwitchBusy(true)
119
+ try {
120
+ setState(await api.setShowQuickSwitch(value))
121
+ setError(null)
122
+ } catch (cause) {
123
+ setState(prev => prev === null ? prev : { ...prev, showQuickSwitch: previous })
124
+ setError(cause instanceof Error ? cause.message : String(cause))
125
+ } finally {
126
+ setQuickSwitchBusy(false)
127
+ }
128
+ }, [state])
129
+
113
130
  const models = useMemo(() => state?.models ?? [], [state])
114
131
 
115
132
  return (
116
133
  <div className="mr-root">
117
134
  <p className="mr-description">{t('settingsDescription')}</p>
135
+ <label className="mr-quick-switch">
136
+ <input
137
+ type="checkbox"
138
+ checked={state?.showQuickSwitch ?? true}
139
+ disabled={loading || quickSwitchBusy}
140
+ onChange={(event) => void toggleQuickSwitch(event.target.checked)}
141
+ />
142
+ <span className="mr-quick-switch-copy">
143
+ <span className="mr-quick-switch-label">{t('showQuickSwitchLabel')}</span>
144
+ <span className="mr-quick-switch-desc">{t('showQuickSwitchDescription')}</span>
145
+ </span>
146
+ </label>
118
147
  <div className="mr-toolbar">
119
148
  <span className="mr-count">{t('modelsCount', { count: models.length })}</span>
120
149
  <button type="button" className="mr-button" onClick={() => void load()} disabled={loading}>
package/src/server/rpc.ts CHANGED
@@ -134,6 +134,14 @@ export function registerApiRoutes(ctx: Context, service: ModelRouterService): ()
134
134
  return service.state()
135
135
  })
136
136
 
137
+ route('set-show-quick-switch', async (p) => {
138
+ if (typeof p.value !== 'boolean') {
139
+ throw new Error('value 必须是布尔值')
140
+ }
141
+ await service.setShowQuickSwitch(p.value)
142
+ return service.state()
143
+ })
144
+
137
145
  return () => {
138
146
  for (const dispose of disposers) dispose()
139
147
  }
@@ -29,12 +29,17 @@ export const RouterConfigSchema: z<RouterConfigShape> = z.object({
29
29
  order: z.array(z.string()),
30
30
  active: z.string(),
31
31
  })),
32
+ // Older documents predate the toggle; normalizeConfig defaults to true.
33
+ // Schemastery object keys are input-optional, so absent is valid here.
34
+ showQuickSwitch: z.boolean(),
32
35
  })
33
36
 
34
- const EMPTY_CONFIG: RouterConfigShape = { models: {} }
37
+ const EMPTY_CONFIG: RouterConfigShape = { models: {}, showQuickSwitch: true }
35
38
 
36
39
  /** The settings-page state view served by the API. */
37
40
  export interface ModelRouterState {
41
+ /** Whether the composer quick route-switcher button is enabled. */
42
+ showQuickSwitch: boolean
38
43
  providers: Array<{ id: string; name: string; credentialConfigured: boolean }>
39
44
  models: Array<{
40
45
  id: string
@@ -159,7 +164,7 @@ export class ModelRouterService implements RouterFacts {
159
164
  }
160
165
  if (!changed) return
161
166
  try {
162
- await this.scope.replace({ models: next })
167
+ await this.scope.replace({ models: next, showQuickSwitch: this.config().showQuickSwitch })
163
168
  } catch (error) {
164
169
  this.ctx.logger.warn(`model-router: reconcile persist failed: ${error instanceof Error ? error.message : String(error)}`)
165
170
  }
@@ -181,6 +186,7 @@ export class ModelRouterService implements RouterFacts {
181
186
  const names = catalog.providerNames
182
187
  const creds = catalog.credentialConfigured
183
188
  return {
189
+ showQuickSwitch: config.showQuickSwitch,
184
190
  providers: catalog.providers.map(provider => ({
185
191
  id: provider.id,
186
192
  name: names.get(provider.id) ?? provider.id,
@@ -230,10 +236,20 @@ export class ModelRouterService implements RouterFacts {
230
236
  await this.persist(models)
231
237
  }
232
238
 
239
+ /** Toggle the composer quick route-switcher button (global, persisted). */
240
+ async setShowQuickSwitch(value: boolean): Promise<void> {
241
+ if (this.scope === undefined) {
242
+ throw new Error('model-router: settings service is not available in this profile')
243
+ }
244
+ const current = this.config()
245
+ if (current.showQuickSwitch === value) return
246
+ await this.scope.replace({ models: current.models, showQuickSwitch: value })
247
+ }
248
+
233
249
  private async persist(models: Record<string, { order: string[]; active: string }>): Promise<void> {
234
250
  if (this.scope === undefined) {
235
251
  throw new Error('model-router: settings service is not available in this profile')
236
252
  }
237
- await this.scope.replace({ models })
253
+ await this.scope.replace({ models, showQuickSwitch: this.config().showQuickSwitch })
238
254
  }
239
255
  }
@@ -56,6 +56,12 @@ export interface RouterModelConfig {
56
56
  /** The whole router config document. */
57
57
  export interface RouterConfigShape {
58
58
  models: Record<string, RouterModelConfig>
59
+ /**
60
+ * Whether the quick route-switcher button is shown beside the composer
61
+ * model selector (settings → 模型路由 → 显示快速路由切换按钮). Stored
62
+ * globally; absent/unknown documents normalize to `true`.
63
+ */
64
+ showQuickSwitch: boolean
59
65
  }
60
66
 
61
67
  export const ROUTER_PROVIDER_ID = 'model-router'
@@ -216,9 +222,11 @@ export function setOrder(
216
222
  /** Shape-validate an unknown value as a RouterConfigShape (lenient). */
217
223
  export function normalizeConfig(value: unknown): RouterConfigShape {
218
224
  const models: Record<string, RouterModelConfig> = {}
219
- if (typeof value !== 'object' || value === null) return { models }
220
- const raw = (value as { models?: unknown }).models
221
- if (typeof raw !== 'object' || raw === null) return { models }
225
+ if (typeof value !== 'object' || value === null) return { models, showQuickSwitch: true }
226
+ const root = value as { models?: unknown; showQuickSwitch?: unknown }
227
+ const showQuickSwitch = root.showQuickSwitch !== false
228
+ const raw = root.models
229
+ if (typeof raw !== 'object' || raw === null) return { models, showQuickSwitch }
222
230
  for (const [id, entry] of Object.entries(raw as Record<string, unknown>)) {
223
231
  if (typeof entry !== 'object' || entry === null) continue
224
232
  const e = entry as { order?: unknown; active?: unknown }
@@ -227,5 +235,5 @@ export function normalizeConfig(value: unknown): RouterConfigShape {
227
235
  if (active === '') continue
228
236
  models[id] = { order, active }
229
237
  }
230
- return { models }
238
+ return { models, showQuickSwitch }
231
239
  }