Merge pull request #5127 from deepseek-harness/fix/account-model-settings

fix(web): 修复账号模型设置的 namespace 识别
This commit is contained in:
lsdsjy authored and GitHub committed 2026-09-24 17:43:32 +08:00
commit 62db4c88c9
6 files changed
+127 -11

No files matched your search

@@ -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: 65e9d8822b67b6c2
zh: 7e83be9defda28ee
/deepseek-ai-dsh-client-ui-settings-models/understand-the-implementation:
en: a87afbec5e339b36
zh: 6348c81af7e650e1
+1 -1
View File
@@ -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 settings section (`llm-deepseek-account` by default), 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.
-----
@@ -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` 段。账号可用模型目录为空时隐藏账号行,包括登录前和退登后;账号模型恢复可用时重新显示。
-----
@@ -182,8 +182,9 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
const node = useMemo(() => schema.nodeAtPath(root, settingsPath), [root, schema, settingsPath])
const fallback = schema.getPath(namespace.value, settingsPath)
const disabled = props.readOnly || busy
const layout = layoutOf(namespace.ns)
const accountProvider = props.provider === 'deepseek-account'
// Account settings use a configurable Cordis entry id.
const layout = accountProvider ? 'deepseek' : layoutOf(namespace.ns)
const keyRef = refFor(schema, namespace, settingsPath, props.provider)
// The same schema read the create card makes, so the choices offered here
// and there cannot drift apart: both come from the adapter's own `Config`.
@@ -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 settings namespace, under a composition's entry id. */
function accountNamespace(ns = 'llm-deepseek-account'): SettingsNamespaceView {
return { ...wireNamespaces().find(view => view.ns === 'llm-deepseek-account')!, ns }
}
/** Credentials answers over the Remote carrier, which has no envelope. */
function remoteOk<T>(value: T) {
return { ok: true as const, value }
@@ -1884,18 +1904,103 @@ describe('apiKeyFailure', () => {
})
it.each([en, zh])('edits the account model catalog without credential or endpoint fields', async (copy) => {
const scripted = scriptedFace({})
const mutate = vi.fn(() => Promise.resolve(remoteOk(accountNamespace())))
const scripted = scriptedFace({ mutate })
const ops = operationsWith(scripted.face)
const describe = vi.spyOn(ops, 'describeCredential')
const namespace = accountNamespace()
const onClose = vi.fn()
render(<ProviderEditor provider="deepseek-account" displayName={copy.deepSeekAccount}
namespace={wireNamespaces()[0]!} settingsPath={[]} schema={settingsSchema}
operations={ops} t={key => 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('keeps the DeepSeek editor for an account route under a renamed settings entry', async () => {
const namespace = accountNamespace('team-account-entry')
const mutate = vi.fn(() => Promise.resolve(remoteOk(namespace)))
const ops = operationsWith(scriptedFace({ mutate }).face)
const set = vi.spyOn(ops, 'storeCredential')
const onClose = vi.fn()
render(<ProviderEditor provider="deepseek-account" displayName={en.deepSeekAccount}
namespace={namespace} settingsPath={[]} schema={settingsSchema}
operations={ops} t={t} readOnly={false} onClose={onClose} />)
expect(screen.queryByText(content => content.includes(en.advancedHint))).toBeNull()
expect(screen.getByDisplayValue('deepseek-v4-flash')).toBeTruthy()
fireEvent.change(screen.getAllByLabelText(new RegExp(en.modelId))[0]!, { target: { value: 'deepseek-v4-mini' } })
fireEvent.click(screen.getByText(en.apply))
await waitFor(() => { expect(onClose).toHaveBeenCalledWith(true) })
expect(mutate).toHaveBeenCalledWith('team-account-entry', expect.any(Array), 0)
expect(set).not.toHaveBeenCalled()
})
it('opens the account row from the section and saves to its own namespace', async () => {
const namespace = accountNamespace()
const mutate = vi.fn(() => Promise.resolve(remoteOk(namespace)))
const { controller, set } = await mountSection({ mutate })
const row = controller.store.getSnapshot().rows[0]!
await act(async () => { controller.store.update((state) => {
state.rows = [{
...row,
accountAvailable: true,
entry: {
provider: 'deepseek-account', displayName: en.deepSeekAccount, settingsNs: namespace.ns, settingsPath: [], active: true,
},
}, ...state.rows]
}) })
fireEvent.click(screen.getByRole('button', {
name: providerCopy(en.editProvider, { provider: 'deepseek-account', displayName: en.deepSeekAccount }),
}))
expect(screen.getByDisplayValue('deepseek-v4-flash')).toBeTruthy()
fireEvent.change(screen.getAllByLabelText(new RegExp(en.modelId))[0]!, { target: { value: 'deepseek-v4-mini' } })
fireEvent.click(screen.getByText(en.apply))
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
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 +2012,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(<ProviderEditor provider="deepseek-account" displayName={en.deepSeekAccount}
namespace={{ ...namespace, value: { models: [] }, base: { models: [] } }} settingsPath={[]} schema={settingsSchema}
operations={operationsWith(scripted.face)} t={t} readOnly={false} onClose={() => {}} />)
@@ -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)