后台 AGC 模型页去掉别名列,改为维护上游原始模型名
- adminApiTypes:AdminAgcModel 改为 {model, enabled},目录改为 {revision, defaultModel, models}
- AdminAgcModelsPage:列为模型名/启用/默认/操作,行按下标定位,重命名默认行时同步 defaultModel,默认行不可停用或删除
- 页面文案改为说明目录初始值由服务端启动时从上游同步
This commit is contained in:
@@ -1075,16 +1075,16 @@ export interface AdminRechargeRefundActionResponse {
|
||||
export interface AdminWalletRestrictionResponse {
|
||||
wallet: AdminProfileWalletPayload;
|
||||
}
|
||||
/** AGC 平台模型目录项:`model` 就是上游原始模型名。 */
|
||||
export interface AdminAgcModel {
|
||||
id: string;
|
||||
alias: string;
|
||||
modelId: string;
|
||||
model: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
/** AGC 平台模型目录:开放给客户端的模型、启用状态与默认项。 */
|
||||
export interface AdminAgcModelCatalog {
|
||||
revision: number;
|
||||
defaultModelId: string;
|
||||
defaultModel: string;
|
||||
models: AdminAgcModel[];
|
||||
}
|
||||
|
||||
|
||||
@@ -25,13 +25,11 @@ vi.mock('../components/useAdminWriteConfirm', () => ({
|
||||
}));
|
||||
afterEach(cleanup);
|
||||
|
||||
test('edits alias and upstream model without changing the stable identifier or revision', async () => {
|
||||
test('renames a model name while keeping revision and row order', async () => {
|
||||
const catalog = {
|
||||
revision: 3,
|
||||
defaultModelId: 'quality',
|
||||
models: [
|
||||
{ id: 'quality', alias: '高质量', modelId: 'gpt-6-astra', enabled: true },
|
||||
],
|
||||
defaultModel: 'gpt-5.6-sol',
|
||||
models: [{ model: 'gpt-5.6-sol', enabled: true }],
|
||||
};
|
||||
vi.mocked(getAgcModelCatalog).mockResolvedValue(catalog);
|
||||
vi.mocked(saveAgcModelCatalog).mockImplementation(async (_, input) => ({
|
||||
@@ -39,18 +37,56 @@ test('edits alias and upstream model without changing the stable identifier or r
|
||||
revision: 4,
|
||||
}));
|
||||
render(<AdminAgcModelsPage token="test" onUnauthorized={vi.fn()} />);
|
||||
await screen.findByDisplayValue('gpt-6-astra');
|
||||
fireEvent.change(screen.getByLabelText('模型 1 别名'), {
|
||||
target: { value: '精细创作' },
|
||||
await screen.findByDisplayValue('gpt-5.6-sol');
|
||||
fireEvent.change(screen.getByLabelText('模型 1 模型名'), {
|
||||
target: { value: 'gpt-5.6-terra' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
||||
await waitFor(() =>
|
||||
expect(saveAgcModelCatalog).toHaveBeenCalledWith('test', {
|
||||
...catalog,
|
||||
models: [{ ...catalog.models[0], alias: '精细创作' }],
|
||||
revision: 3,
|
||||
defaultModel: 'gpt-5.6-terra',
|
||||
models: [{ model: 'gpt-5.6-terra', enabled: true }],
|
||||
}),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('已保存').length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
test('keeps the default model on the renamed row and appends an enabled blank row', async () => {
|
||||
const catalog = {
|
||||
revision: 3,
|
||||
defaultModel: 'gpt-5.6-sol',
|
||||
models: [
|
||||
{ model: 'gpt-5.6-sol', enabled: true },
|
||||
{ model: 'gpt-5.6-luna', enabled: true },
|
||||
],
|
||||
};
|
||||
vi.mocked(getAgcModelCatalog).mockResolvedValue(catalog);
|
||||
vi.mocked(saveAgcModelCatalog).mockImplementation(async (_, input) => input);
|
||||
render(<AdminAgcModelsPage token="test" onUnauthorized={vi.fn()} />);
|
||||
await screen.findByDisplayValue('gpt-5.6-luna');
|
||||
|
||||
fireEvent.click(screen.getByLabelText('模型 2 默认'));
|
||||
fireEvent.change(screen.getByLabelText('模型 2 模型名'), {
|
||||
target: { value: 'gpt-5.6-verda' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '添加模型' }));
|
||||
fireEvent.change(screen.getByLabelText('模型 3 模型名'), {
|
||||
target: { value: 'gpt-5.6-nova' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(saveAgcModelCatalog).toHaveBeenCalledWith('test', {
|
||||
revision: 3,
|
||||
defaultModel: 'gpt-5.6-verda',
|
||||
models: [
|
||||
{ model: 'gpt-5.6-sol', enabled: true },
|
||||
{ model: 'gpt-5.6-verda', enabled: true },
|
||||
{ model: 'gpt-5.6-nova', enabled: true },
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CircleHelp, Plus, RefreshCcw, Save, Trash2 } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { getAgcModelCatalog, saveAgcModelCatalog } from '../api/adminApiClient';
|
||||
import type { AdminAgcModel, AdminAgcModelCatalog } from '../api/adminApiTypes';
|
||||
import type { AdminAgcModelCatalog } from '../api/adminApiTypes';
|
||||
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
|
||||
import { handlePageError } from './pageUtils';
|
||||
|
||||
@@ -38,19 +38,38 @@ export function AdminAgcModelsPage({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
function update(id: string, patch: Partial<AdminAgcModel>) {
|
||||
/** 目录项没有稳定标识,一律按下标定位。 */
|
||||
function setEnabled(index: number, enabled: boolean) {
|
||||
setSaved(false);
|
||||
setCatalog(
|
||||
(current) =>
|
||||
current && {
|
||||
...current,
|
||||
models: current.models.map((model) =>
|
||||
model.id === id ? { ...model, ...patch } : model,
|
||||
models: current.models.map((model, candidate) =>
|
||||
candidate === index ? { ...model, enabled } : model,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 重命名默认项时必须同步默认模型名,否则服务端会因默认项不在目录中而拒绝保存。 */
|
||||
function renameModel(index: number, model: string) {
|
||||
setSaved(false);
|
||||
setCatalog(
|
||||
(current) =>
|
||||
current && {
|
||||
...current,
|
||||
models: current.models.map((entry, candidate) =>
|
||||
candidate === index ? { ...entry, model } : entry,
|
||||
),
|
||||
defaultModel:
|
||||
current.models[index]?.model === current.defaultModel
|
||||
? model
|
||||
: current.defaultModel,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!catalog || busy) return;
|
||||
if (
|
||||
@@ -78,7 +97,7 @@ export function AdminAgcModelsPage({
|
||||
<div className="admin-page-heading">
|
||||
<div>
|
||||
<h2>AGC 模型</h2>
|
||||
<p>管理客户端可用模型与用户看到的名称</p>
|
||||
<p>维护平台开放给客户端的模型、启用状态与默认项</p>
|
||||
</div>
|
||||
<span className="admin-agc-models-revision">
|
||||
版本 v{catalog?.revision ?? '-'}
|
||||
@@ -93,13 +112,7 @@ export function AdminAgcModelsPage({
|
||||
</div>
|
||||
<div>
|
||||
<span>默认模型</span>
|
||||
<strong>
|
||||
{catalog
|
||||
? (catalog.models.find(
|
||||
(model) => model.id === catalog.defaultModelId,
|
||||
)?.alias ?? '未设置')
|
||||
: '未设置'}
|
||||
</strong>
|
||||
<strong>{catalog?.defaultModel || '未设置'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>发布状态</span>
|
||||
@@ -110,7 +123,9 @@ export function AdminAgcModelsPage({
|
||||
<div className="admin-panel-heading">
|
||||
<div>
|
||||
<h3>模型目录</h3>
|
||||
<span>客户端仅显示别名,实际模型名仅在这里维护</span>
|
||||
<span>
|
||||
目录初始值由服务端启动时从上游同步,这里维护平台开放给客户端的模型、启用状态与默认项
|
||||
</span>
|
||||
</div>
|
||||
<CircleHelp size={17} aria-label="模型目录帮助" />
|
||||
</div>
|
||||
@@ -135,15 +150,7 @@ export function AdminAgcModelsPage({
|
||||
(current) =>
|
||||
current && {
|
||||
...current,
|
||||
models: [
|
||||
...current.models,
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
alias: '',
|
||||
modelId: '',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
models: [...current.models, { model: '', enabled: true }],
|
||||
},
|
||||
);
|
||||
}}
|
||||
@@ -166,85 +173,80 @@ export function AdminAgcModelsPage({
|
||||
<table className="admin-table admin-agc-models-table-grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>别名</th>
|
||||
<th>实际模型名</th>
|
||||
<th>模型名</th>
|
||||
<th>启用</th>
|
||||
<th>默认</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{catalog?.models.map((model, index) => (
|
||||
<tr key={model.id}>
|
||||
<td>
|
||||
<input
|
||||
aria-label={`模型 ${index + 1} 别名`}
|
||||
maxLength={40}
|
||||
required
|
||||
value={model.alias}
|
||||
disabled={busy}
|
||||
onChange={(e) =>
|
||||
update(model.id, { alias: e.target.value })
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
aria-label={`模型 ${index + 1} 实际模型名`}
|
||||
maxLength={200}
|
||||
required
|
||||
value={model.modelId}
|
||||
disabled={busy}
|
||||
onChange={(e) =>
|
||||
update(model.id, { modelId: e.target.value })
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
aria-label={`模型 ${index + 1} 启用`}
|
||||
type="checkbox"
|
||||
checked={model.enabled}
|
||||
disabled={busy || model.id === catalog.defaultModelId}
|
||||
onChange={(e) =>
|
||||
update(model.id, { enabled: e.target.checked })
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
aria-label={`模型 ${index + 1} 默认`}
|
||||
name="agc-default-model"
|
||||
type="radio"
|
||||
checked={model.id === catalog.defaultModelId}
|
||||
disabled={busy || !model.enabled}
|
||||
onChange={() => {
|
||||
setSaved(false);
|
||||
setCatalog({ ...catalog, defaultModelId: model.id });
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
title="删除模型"
|
||||
aria-label={`删除模型 ${index + 1}`}
|
||||
disabled={busy || model.id === catalog.defaultModelId}
|
||||
onClick={() => {
|
||||
setSaved(false);
|
||||
setCatalog({
|
||||
...catalog,
|
||||
models: catalog.models.filter(
|
||||
(candidate) => candidate.id !== model.id,
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{catalog?.models.map((model, index) => {
|
||||
const isDefault = catalog.defaultModel === model.model;
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<input
|
||||
aria-label={`模型 ${index + 1} 模型名`}
|
||||
maxLength={200}
|
||||
required
|
||||
value={model.model}
|
||||
disabled={busy}
|
||||
onChange={(e) => renameModel(index, e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
aria-label={`模型 ${index + 1} 启用`}
|
||||
type="checkbox"
|
||||
checked={model.enabled}
|
||||
disabled={busy || isDefault}
|
||||
onChange={(e) => setEnabled(index, e.target.checked)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
aria-label={`模型 ${index + 1} 默认`}
|
||||
name="agc-default-model"
|
||||
type="radio"
|
||||
checked={isDefault}
|
||||
disabled={busy || !model.enabled}
|
||||
onChange={() => {
|
||||
setSaved(false);
|
||||
setCatalog(
|
||||
(current) =>
|
||||
current && {
|
||||
...current,
|
||||
defaultModel: model.model,
|
||||
},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
title="删除模型"
|
||||
aria-label={`删除模型 ${index + 1}`}
|
||||
disabled={busy || isDefault}
|
||||
onClick={() => {
|
||||
setSaved(false);
|
||||
setCatalog(
|
||||
(current) =>
|
||||
current && {
|
||||
...current,
|
||||
models: current.models.filter(
|
||||
(_, candidate) => candidate !== index,
|
||||
),
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -3816,7 +3816,7 @@ button:disabled {
|
||||
color: #fff;
|
||||
}
|
||||
.admin-agc-models-table-grid {
|
||||
min-width: 720px;
|
||||
min-width: 540px;
|
||||
}
|
||||
.admin-agc-models-table-grid th {
|
||||
padding-top: 12px;
|
||||
|
||||
Reference in New Issue
Block a user