From 2630b0f6906cbdf7740d45ae149609a4adb3aba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Sat, 3 Oct 2026 16:29:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=BC=9A=E5=91=98):=20=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=20AGC=20=E6=A8=A1=E5=9E=8B=E6=94=AF=E6=8C=81=E6=A0=87=E6=B3=A8?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AdminAgcModel 新增 access 字段(缺省 basic,失败开放),GET 回读 PUT 写入 - api-server 保存时校验权限档只允许 basic/full - 后台 AGC 模型页新增「权限档」列,可按模型选择基础/高性能模型 - fake API 与页面用例覆盖权限档选择与保存 --- apps/admin-web/src/api/adminApiTypes.ts | 3 ++ .../src/pages/AdminAgcModelsPage.test.tsx | 36 +++++++++++++++++++ .../src/pages/AdminAgcModelsPage.tsx | 17 +++++++++ scripts/admin-web-fake-api.mjs | 3 ++ server-rs/crates/api-server/src/agc_models.rs | 10 ++++-- .../crates/shared-contracts/src/admin.rs | 8 +++++ 6 files changed, 75 insertions(+), 2 deletions(-) diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index 715d40d64..ceb6db413 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -1174,6 +1174,7 @@ export interface AdminWalletRestrictionResponse { wallet: AdminProfileWalletPayload; } export type AdminAgcAgentMode = 'codex' | 'cc'; +export type AdminAgcModelAccess = 'basic' | 'full'; export type AdminAgcModelProtocol = | 'openai_responses' | 'openai_chat' @@ -1187,6 +1188,8 @@ export interface AdminAgcModel { agentMode: AdminAgcAgentMode; /** 上游线协议,与执行器正交。 */ protocol?: AdminAgcModelProtocol; + /** 该模型要求的会员档位,缺省 `basic`(失败开放)。 */ + access?: AdminAgcModelAccess; } export interface AdminAgcModelCatalog { diff --git a/apps/admin-web/src/pages/AdminAgcModelsPage.test.tsx b/apps/admin-web/src/pages/AdminAgcModelsPage.test.tsx index feb4ce9d3..3279a5342 100644 --- a/apps/admin-web/src/pages/AdminAgcModelsPage.test.tsx +++ b/apps/admin-web/src/pages/AdminAgcModelsPage.test.tsx @@ -101,10 +101,46 @@ test('new models default to Codex and save the selected Agent mode', async () => modelId: '', enabled: true, agentMode: 'cc', + access: 'basic', }); }); }); +test('权限档可按模型单独选择并随目录保存', async () => { + const catalog = { + revision: 3, + defaultModelId: 'quality', + models: [ + { + id: 'quality', + alias: '高质量', + modelId: 'gpt-6-astra', + enabled: true, + agentMode: 'codex' as const, + access: 'basic' as const, + }, + ], + }; + vi.mocked(getAgcModelCatalog).mockResolvedValue(catalog); + vi.mocked(saveAgcModelCatalog).mockImplementation(async (_, input) => ({ + ...input, + revision: 4, + })); + render(); + await screen.findByDisplayValue('gpt-6-astra'); + + const access = screen.getByLabelText('模型 1 权限档') as HTMLSelectElement; + expect(access.value).toBe('basic'); + fireEvent.change(access, { target: { value: 'full' } }); + fireEvent.click(screen.getByRole('button', { name: '保存' })); + + await waitFor(() => { + const calls = vi.mocked(saveAgcModelCatalog).mock.calls; + const payload = calls[calls.length - 1]?.[1]; + expect(payload?.models[0]).toMatchObject({ access: 'full' }); + }); +}); + test('协议与 Agent 模式是独立字段,可按模型单独选择', async () => { const catalog = { revision: 3, diff --git a/apps/admin-web/src/pages/AdminAgcModelsPage.tsx b/apps/admin-web/src/pages/AdminAgcModelsPage.tsx index b009aa222..9e14400cb 100644 --- a/apps/admin-web/src/pages/AdminAgcModelsPage.tsx +++ b/apps/admin-web/src/pages/AdminAgcModelsPage.tsx @@ -118,6 +118,7 @@ export function AdminAgcModelsPage({ { key: 'modelId', label: '实际模型名' }, { key: 'agentMode', label: 'Agent 模式' }, { key: 'protocol', label: '协议' }, + { key: 'access', label: '权限档' }, { key: 'enabled', label: '启用' }, { key: 'default', label: '默认' }, { key: 'actions', label: '操作' }, @@ -159,6 +160,7 @@ export function AdminAgcModelsPage({ enabled: true, agentMode: 'codex', protocol: 'openai_responses', + access: 'basic', }, ], }, @@ -246,6 +248,21 @@ export function AdminAgcModelsPage({ + + + , AppError>>()?; @@ -297,6 +302,7 @@ fn catalog_dto(catalog: AgcModelCatalog) -> AdminAgcModelCatalog { enabled: m.enabled, agent_mode: m.agent_mode.as_str().to_string(), protocol: Some(protocol), + access: m.access.as_str().to_string(), } }) .collect(), diff --git a/server-rs/crates/shared-contracts/src/admin.rs b/server-rs/crates/shared-contracts/src/admin.rs index 6516d4e20..a0ff50bbd 100644 --- a/server-rs/crates/shared-contracts/src/admin.rs +++ b/server-rs/crates/shared-contracts/src/admin.rs @@ -1704,6 +1704,11 @@ fn default_codex_agent_mode() -> String { "codex".to_string() } +/// 存量后台请求缺省为 `basic`(失败开放),只有人工标注才收紧到 `full`。 +fn default_basic_agc_model_access() -> String { + "basic".to_string() +} + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AdminAgcModel { @@ -1718,6 +1723,9 @@ pub struct AdminAgcModel { /// 服务端回读时始终给出解析后的具体值。 #[serde(default, skip_serializing_if = "Option::is_none")] pub protocol: Option, + /// 该模型要求的会员档位:`basic`(缺省,对全部档位开放)/ `full`(后台人工标注)。 + #[serde(default = "default_basic_agc_model_access")] + pub access: String, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]