feat(models): 平台自建「模型设置」—— 用户自配厂家 / 条目各自开关 / 共享模型开关(档案 87)
- 官方「设置 → 模型」页在平台环境**必然报错**(判据在**浏览器页面**的 loopback 判定;官方 README 原文 Non-loopback pages get no durable settings)⇒ 该分区对全角色(含 admin)隐藏,用户自配改走平台自建页(插件 0.3.11)
- DB 迁移 V6:credential_vault.route/base_url/api/models + users.shared_model_enabled;并**重定义 getEnabledCredentialKeyRef**(互斥删除后原实现无 ORDER BY ⇒ 「任取一条」)
- 新落地层 src/web/model-landing.ts:spawn 时把「已启用条目」写进实例 .credentials.yaml 与 settings.yaml 的 llm-pi-ai.providers.<route>;字段名与官方包实测对齐(apiKeyEnv / baseURL / api,**不是** protocol);只碰自己写过的 + 一次性交接
- 接口 /api/me/keys、/api/me/keys/:id/toggle、/api/me/models/shared;前端新增「设置 → 模型设置」分区(settings.section id=model-settings / order 100 / 全角色)
- 顺手修两处:ensure-role-profile-patch.cjs 的 --force 整文件覆盖会抹掉 admin 的 disable-hmr 与 workspace-scoped-picker 两个平台块(改为 stripManagedBlock 只替换自己那段);verify-mem-model.mjs 因档案 86 重构而长期失败的陈旧断言
⚠️ 本提交同时包含**档案 86(admin 跨用户实例管理 + 两处改名)**的代码改动 —— 该部分已上线并端到端验证;其 import/register 与本次改动同处 src/web/server.ts、poc/business-plugins/lib/client.js 等文件,按**文件粒度无法拆分**,且不带它会让仓库 tsc 直接失败(缺 src/web/routes/admin-user-ops.ts)。
This commit is contained in:
1 parent
eb50ca2d5b
commit
918f1d3a51
20 files changed
+2073
-276
No files matched your search
+71
-5
@@ -40,7 +40,8 @@ function register(backend, makeAdapter) {
|
||||
}
|
||||
})
|
||||
|
||||
test(`${backend}: concurrent setCredentialKey keeps exactly one enabled`, async () => {
|
||||
// 档案 87:条目口径从「互斥单选」改为「**各自开关、可同时启用**」⇒ 老断言整体改写。
|
||||
test(`${backend}: concurrent setCredentialKey keeps every entry enabled (不再互斥)`, async () => {
|
||||
const db = await makeAdapter()
|
||||
try {
|
||||
await db.createUser(user('a', 'alice'))
|
||||
@@ -48,7 +49,10 @@ function register(backend, makeAdapter) {
|
||||
Array.from({ length: 5 }, (_, i) => db.setCredentialKey('a', `k${i}`, `ref${i}`)),
|
||||
)
|
||||
const keys = await db.listCredentialKeys('a')
|
||||
assert.equal(keys.filter((k) => k.enabled).length, 1, 'exactly one key enabled')
|
||||
assert.equal(keys.length, 5, 'five rows')
|
||||
// 写新条目**不再**把其它条目全关(老实现是 `SET enabled = 0 WHERE user_id = ?`)。
|
||||
assert.equal(keys.filter((k) => k.enabled).length, 5, 'every entry stays enabled')
|
||||
assert.equal((await db.listEnabledCredentialKeys('a')).length, 5, 'listEnabled agrees with list')
|
||||
const ref = await db.getEnabledCredentialKeyRef('a')
|
||||
assert.ok(ref !== null && ref.startsWith('ref'), 'enabled key has a ref')
|
||||
} finally {
|
||||
@@ -56,15 +60,77 @@ function register(backend, makeAdapter) {
|
||||
}
|
||||
})
|
||||
|
||||
test(`${backend}: selectCredentialKey flips the enabled key`, async () => {
|
||||
test(`${backend}: toggleCredentialKey 只动一行(不影响其它条目)`, async () => {
|
||||
const db = await makeAdapter()
|
||||
try {
|
||||
await db.createUser(user('a', 'alice'))
|
||||
const k1 = await db.setCredentialKey('a', 'k1', 'r1')
|
||||
await db.setCredentialKey('a', 'k2', 'r2')
|
||||
assert.equal(await db.getEnabledCredentialKeyRef('a'), 'r2')
|
||||
assert.equal(await db.toggleCredentialKey('a', k1.id, false), true)
|
||||
const keys = await db.listCredentialKeys('a')
|
||||
assert.equal(keys.find((k) => k.id === k1.id).enabled, false, 'target row off')
|
||||
assert.equal(keys.find((k) => k.name === 'k2').enabled, true, 'the other row untouched')
|
||||
assert.equal((await db.listEnabledCredentialKeys('a')).length, 1, 'only one enabled now')
|
||||
// 不存在的 id ⇒ false(路由据此回 404)
|
||||
assert.equal(await db.toggleCredentialKey('a', 'nope', true), false)
|
||||
} finally {
|
||||
await db.close()
|
||||
}
|
||||
})
|
||||
|
||||
test(`${backend}: getEnabledCredentialKeyRef 只认内置条目(档案 87 语义重定义)`, async () => {
|
||||
const db = await makeAdapter()
|
||||
try {
|
||||
await db.createUser(user('a', 'alice'))
|
||||
// 自定义厂家(给了 baseUrl)**不是**"用户自己的 DeepSeek key",否则 keySourceOf /
|
||||
// resolveApiKey 会把一个网关的 key 当成平台内置 key 用。
|
||||
await db.setCredentialKey('a', 'gw', 'gwref', {
|
||||
route: 'my-gw',
|
||||
baseUrl: 'https://api.example.com/v1',
|
||||
api: 'openai-completions',
|
||||
models: '["gpt-4o"]',
|
||||
})
|
||||
assert.equal(await db.getEnabledCredentialKeyRef('a'), null, 'custom provider is not the builtin ref')
|
||||
await db.setCredentialKey('a', 'ds', 'dsref')
|
||||
assert.equal(await db.getEnabledCredentialKeyRef('a'), 'dsref', 'builtin entry wins')
|
||||
// 元数据必须原样存回来(route / baseUrl / api / models)
|
||||
const gw = (await db.listCredentialKeys('a')).find((k) => k.name === 'gw')
|
||||
assert.equal(gw.route, 'my-gw')
|
||||
assert.equal(gw.baseUrl, 'https://api.example.com/v1')
|
||||
assert.equal(gw.api, 'openai-completions')
|
||||
assert.equal(gw.models, '["gpt-4o"]')
|
||||
} finally {
|
||||
await db.close()
|
||||
}
|
||||
})
|
||||
|
||||
test(`${backend}: sharedModelEnabled 默认开、可关(档案 87 口径②)`, async () => {
|
||||
const db = await makeAdapter()
|
||||
try {
|
||||
await db.createUser(user('a', 'alice'))
|
||||
assert.equal(await db.getSharedModelEnabled('a'), true, 'V6 默认 true')
|
||||
assert.equal(await db.setSharedModelEnabled('a', false), true)
|
||||
assert.equal(await db.getSharedModelEnabled('a'), false)
|
||||
assert.equal(await db.setSharedModelEnabled('a', true), true)
|
||||
assert.equal(await db.getSharedModelEnabled('a'), true)
|
||||
} finally {
|
||||
await db.close()
|
||||
}
|
||||
})
|
||||
|
||||
test(`${backend}: selectCredentialKey 不再关掉别的条目(档案 87)`, async () => {
|
||||
const db = await makeAdapter()
|
||||
try {
|
||||
await db.createUser(user('a', 'alice'))
|
||||
const k1 = await db.setCredentialKey('a', 'k1', 'r1')
|
||||
const k2 = await db.setCredentialKey('a', 'k2', 'r2')
|
||||
// 两条内置条目都启用 ⇒ 取"最新一条"(两条写在同一毫秒时由 id 决定,故这里只断非空)
|
||||
assert.ok((await db.getEnabledCredentialKeyRef('a')) !== null)
|
||||
assert.equal(await db.selectCredentialKey('a', k1.id), true)
|
||||
assert.equal(await db.getEnabledCredentialKeyRef('a'), 'r1')
|
||||
// 老语义会先把所有条目关掉再开这一个;新语义只保证"这一个开"。
|
||||
const keys = await db.listCredentialKeys('a')
|
||||
assert.equal(keys.find((k) => k.id === k2.id).enabled, true, 'another entry is not switched off')
|
||||
assert.equal(keys.find((k) => k.id === k1.id).enabled, true, 'target stays enabled')
|
||||
} finally {
|
||||
await db.close()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user