diff --git a/packages/client/ui-settings-models/README.i18n.yaml b/packages/client/ui-settings-models/README.i18n.yaml index 79acecbefe..9af1829753 100644 --- a/packages/client/ui-settings-models/README.i18n.yaml +++ b/packages/client/ui-settings-models/README.i18n.yaml @@ -30,8 +30,8 @@ en: 589397a35f82244e zh: 342fbceee2c173a2 /deepseek-ai-dsh-client-ui-settings-models/use-this-package/extension-slots: - en: 6979796f0332268f - zh: 00896a005cc4f4e2 + en: 1edb9186d1ab73c2 + zh: c4895e03947c1235 /deepseek-ai-dsh-client-ui-settings-models/understand-the-implementation: en: a87afbec5e339b36 zh: 6348c81af7e650e1 diff --git a/packages/client/ui-settings-models/README.md b/packages/client/ui-settings-models/README.md index 549177fbbb..b34be2679f 100644 --- a/packages/client/ui-settings-models/README.md +++ b/packages/client/ui-settings-models/README.md @@ -61,7 +61,7 @@ After the versioned notice step completes, the DeepSeek step projects first-run The section declares two seats for plugins distributed outside this repository, typed in [`src/client/slot-contract.ts`](src/client/slot-contract.ts) and exported from `./client`. `settings.models.provider-card` (keyed) renders inside every card that shows a directory row — a saved row's card, its first-run setup posture, and the add-provider draft — dispatched with `entryKey = settingsNs` and owner props carrying the row's `ConfigurableProviderView`, its configured state, and its confirmed api-key credential state, so one registration under an adapter family's namespace receives every card of that family, hand-declared routes included; the hand-declared draft card has no directory row yet and dispatches nothing until saved. `settings.models.footer` (list) renders after the rows and the add controls. A registrant activates through `ctx.slots.inject` with a type-only import of this package's `/client` entry; without registrants both seats render nothing. -The Models page includes **DeepSeek Account** (`deepseek-account`, **DeepSeek 账号** in Chinese). Its editor exposes the shared DeepSeek model catalog without API-key or base-URL inputs. The account row is hidden when its available model catalog is empty, including before sign-in and after sign-out; it returns when account models become available. +The Models page includes **DeepSeek Account** (`deepseek-account`, **DeepSeek 账号** in Chinese). Its editor exposes the shared DeepSeek model catalog without API-key or base-URL inputs, and saves that catalog under the account route's own `llm-deepseek-account` section, so an account edit never rewrites the `llm-deepseek` section the official route reads. The account row is hidden when its available model catalog is empty, including before sign-in and after sign-out; it returns when account models become available. ----- diff --git a/packages/client/ui-settings-models/README.zh.md b/packages/client/ui-settings-models/README.zh.md index e9bd2122c3..d4029f3a19 100644 --- a/packages/client/ui-settings-models/README.zh.md +++ b/packages/client/ui-settings-models/README.zh.md @@ -61,7 +61,7 @@ API 密钥输入框初始为空,并通过 `autocomplete="new-password"` 请求 本分区为仓库外分发的插件声明两个席位,类型定义在 [`src/client/slot-contract.ts`](src/client/slot-contract.ts) 并从 `./client` 导出。`settings.models.provider-card`(keyed)渲染在每张展示目录行的卡片内部——已保存行的卡片、其首次运行 setup 形态、以及「添加提供商」草稿卡——以 `entryKey = settingsNs` 分发,owner props 携带该行的 `ConfigurableProviderView`、其 configured 状态与已确认的 api-key 凭据状态,因此以某适配器家族的 namespace 注册一次即可收到该家族的全部卡片,含手工声明的路由;手工声明的草稿卡尚无目录行,保存之前不分发。`settings.models.footer`(list)渲染在行列表与新增控件之后。注册方通过 `ctx.slots.inject` 激活,并以 type-only import 引入本包 `/client` 入口;没有注册方时两个席位均不渲染任何内容。 -Models 页面包含 **DeepSeek 账号**(`deepseek-account`,英文为 **DeepSeek Account**)。其编辑器展示共享的 DeepSeek 模型目录,不提供 API Key 或 Base URL 输入框。账号可用模型目录为空时隐藏账号行,包括登录前和退登后;账号模型恢复可用时重新显示。 +Models 页面包含 **DeepSeek 账号**(`deepseek-account`,英文为 **DeepSeek Account**)。其编辑器展示共享的 DeepSeek 模型目录,不提供 API Key 或 Base URL 输入框;目录保存到账号路由自己的 `llm-deepseek-account` 段,因此账号侧的编辑不会改写 official 路由读取的 `llm-deepseek` 段。账号可用模型目录为空时隐藏账号行,包括登录前和退登后;账号模型恢复可用时重新显示。 ----- diff --git a/packages/client/ui-settings-models/src/client/ProviderEditor.tsx b/packages/client/ui-settings-models/src/client/ProviderEditor.tsx index a50fe2933f..08056cc965 100644 --- a/packages/client/ui-settings-models/src/client/ProviderEditor.tsx +++ b/packages/client/ui-settings-models/src/client/ProviderEditor.tsx @@ -138,6 +138,7 @@ export function pathOps( /** The editor layout the owning namespace selects. */ function layoutOf(ns: string): EditorLayout { if (ns === 'llm-deepseek') return 'deepseek' + if (ns === 'llm-deepseek-account') return 'deepseek' if (ns === 'llm-pi-ai') return 'pi-ai' return 'unknown' } diff --git a/packages/client/ui-settings-models/tests/components.client.spec.tsx b/packages/client/ui-settings-models/tests/components.client.spec.tsx index 1a2398bd2a..671cc0b5ca 100644 --- a/packages/client/ui-settings-models/tests/components.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/components.client.spec.tsx @@ -137,9 +137,29 @@ function wireNamespaces(): SettingsNamespaceView[] { secrets: [], revision: 4, }, + { + ns: 'llm-deepseek-account', + schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue, + value: { + baseURL: 'https://base', + defaultContextWindow: 1_000_000, + maxTokens: 256_000, + models: DEFAULT_DEEPSEEK_MODELS, + }, + base: { defaultContextWindow: 1_000_000, maxTokens: 256_000, models: DEFAULT_DEEPSEEK_MODELS }, + user: {}, + autoGenerate: true, applies: 'live', + secrets: [], + revision: 0, + }, ] } +/** The account provider's dedicated settings namespace. */ +function accountNamespace(): SettingsNamespaceView { + return wireNamespaces().find(view => view.ns === 'llm-deepseek-account')! +} + /** Credentials answers over the Remote carrier, which has no envelope. */ function remoteOk(value: T) { return { ok: true as const, value } @@ -1887,15 +1907,41 @@ it.each([en, zh])('edits the account model catalog without credential or endpoin const scripted = scriptedFace({}) const ops = operationsWith(scripted.face) const describe = vi.spyOn(ops, 'describeCredential') + const namespace = accountNamespace() + const mutate = vi.spyOn(scripted.face.settings, 'mutate').mockResolvedValue(remoteOk(accountNamespace())) + const onClose = vi.fn() render( copy[key]} readOnly={false} onClose={() => {}} />) + namespace={namespace} settingsPath={[]} schema={settingsSchema} + operations={ops} t={key => copy[key]} readOnly={false} onClose={onClose} />) expect(screen.queryByLabelText(copy.keyInput)).toBeNull() expect(screen.queryByLabelText(copy.baseUrl)).toBeNull() expect(describe).not.toHaveBeenCalled() expect(screen.getByDisplayValue('deepseek-v4-flash')).toBeTruthy() + expect(screen.queryByText(content => content.includes(copy.advancedHint))).toBeNull() await expect(`${document.body.textContent}\n`) .toMatchFileSnapshot(`./expected/deepseek-account-${copy === en ? 'en' : 'zh'}.txt`) + const set = vi.spyOn(ops, 'storeCredential') + fireEvent.change(screen.getAllByLabelText(new RegExp(copy.modelId))[0]!, { target: { value: 'deepseek-v4-mini' } }) + fireEvent.click(screen.getByText(copy.apply)) + await waitFor(() => { expect(onClose).toHaveBeenCalledWith(true) }) + expect(mutate.mock.calls[0]).toEqual([ + 'llm-deepseek-account', + [{ + op: 'set', + path: ['models'], + value: [ + { + id: 'deepseek-v4-mini', + name: 'DeepSeek-V4-Flash', + description: 'Preserved hidden detail', + contextWindow: 1_000_000, + }, + { id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro', contextWindow: 1_000_000 }, + ], + }], + 0, + ]) + expect(set).not.toHaveBeenCalled() }) it('renders the localized account row and supports catalogs without capacity defaults', async () => { @@ -1907,7 +1953,7 @@ it('renders the localized account row and supports catalogs without capacity def }) }) expect(screen.getByText(en.deepSeekAccount)).toBeTruthy() view.unmount() - const namespace = wireNamespaces()[0]! + const namespace = accountNamespace() render( {}} />) diff --git a/packages/client/ui-settings-models/tests/store.client.spec.ts b/packages/client/ui-settings-models/tests/store.client.spec.ts index 79905b4441..007ff68099 100644 --- a/packages/client/ui-settings-models/tests/store.client.spec.ts +++ b/packages/client/ui-settings-models/tests/store.client.spec.ts @@ -62,6 +62,15 @@ const NAMESPACES = [ secrets: [], revision: 0, }, + { + ns: 'llm-deepseek-account', + schema: {}, + value: { baseURL: 'https://base' }, + base: { baseURL: 'https://base' }, + autoGenerate: true, applies: 'live' as const, + secrets: [], + revision: 0, + }, { ns: 'llm-pi-ai', schema: {}, @@ -346,7 +355,7 @@ describe('edge joins', () => { it.each([false, true])('uses account availability without asking for an API key: %s', async (accountAvailable) => { const { ctx, mirror, seenRefs } = api({ accountAvailable, providers: async () => ok({ providers: [{ - provider: 'deepseek-account', displayName: 'DeepSeek Account', settingsNs: 'llm-deepseek', settingsPath: [], active: true, + provider: 'deepseek-account', displayName: 'DeepSeek Account', settingsNs: 'llm-deepseek-account', settingsPath: [], active: true, }] }) }) const store = new ModelsSettingsStore(ctx, settingsSchema, mirror) await store.load() @@ -356,12 +365,13 @@ it.each([false, true])('uses account availability without asking for an API key: expect(rows[0]).toMatchObject({ accountAvailable: true, apiKeyEnv: undefined, credential: undefined }) expect(providerUsable(rows[0]!)).toBe(true) } + expect(store.store.getSnapshot().namespaces.get('llm-deepseek-account')?.ns).toBe('llm-deepseek-account') expect(seenRefs).toEqual([]) }) it('removes the account row after sign-out and restores it after sign-in', async () => { const overrides = { accountAvailable: true, providers: async () => ok({ providers: [{ - provider: 'deepseek-account', displayName: 'DeepSeek Account', settingsNs: 'llm-deepseek', settingsPath: [], active: true, + provider: 'deepseek-account', displayName: 'DeepSeek Account', settingsNs: 'llm-deepseek-account', settingsPath: [], active: true, }, ...DIRECTORY] }) } const { ctx, mirror } = api(overrides) const store = new ModelsSettingsStore(ctx, settingsSchema, mirror)