fix(web): 修复账号模型设置的 namespace 识别
This commit is contained in:
1 parent
a3480857dd
commit
7b86b84c2f
6 files changed
+66
-9
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: 1edb9186d1ab73c2
|
||||
zh: c4895e03947c1235
|
||||
/deepseek-ai-dsh-client-ui-settings-models/understand-the-implementation:
|
||||
en: a87afbec5e339b36
|
||||
zh: 6348c81af7e650e1
|
||||
|
||||
@@ -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.
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -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` 段。账号可用模型目录为空时隐藏账号行,包括登录前和退登后;账号模型恢复可用时重新显示。
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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<T>(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(<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('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(<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)
|
||||
|
||||
Reference in new issue
Block a user