feat(admin): Phase 8 模型供应商管理 — provider/model CRUD + 启停 + 定价 + 设默认
后端:ModelConfig.is_default + migration;get_default_model 优先 is_default 否则回落最早 active(零回归); adminpanel providers CRUD(api_key write-only 不回传)+ models CRUD(?provider/capability 筛)+ set-default(同 capability 清旧); IsPlatformAdmin + 审计。 前端:adminApi providers/models/setDefault;Admin 模型供应商页(供应商表+模型表+启停+定价+设默认+供应商/模型弹窗)。 测试:adminpanel 51 单测过(api_key 隐藏+入库/CRUD/set-default 改 get_default_model); apps.ai 仍仅 3 个进场前既有失败(零回归);无头 e2e _admin-p8.mjs 5 断言过 + 0 console error (用一次性禁用 provider+model capability=export,无副作用,跑完删);tsc+build 绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
39b4467258
commit
4ce7e957d2
@@ -453,3 +453,81 @@ class AdminBillingTests(TestCase):
|
||||
# setUp 本月已有 CHARGE 10 → 再 +1 超 10 → 拦
|
||||
with self.assertRaises(ValueError):
|
||||
_enforce_quota_policy(team=self.team, project=None, amount=Decimal("1"))
|
||||
|
||||
|
||||
class AdminModelProviderTests(TestCase):
|
||||
"""Phase 8:模型供应商 / 模型 CRUD + 启停 + 定价 + 设默认(改变 get_default_model)+ 权限。"""
|
||||
|
||||
def setUp(self):
|
||||
from apps.ai.models import ModelConfig, ModelProvider
|
||||
|
||||
self.admin = User.objects.create_user(username="padmin8", password="x", is_platform_admin=True)
|
||||
self.normal = User.objects.create_user(username="normal8", password="x")
|
||||
self.prov = ModelProvider.objects.create(name="prov8", display_name="P8", api_key="secret-key")
|
||||
self.m1 = ModelConfig.objects.create(provider=self.prov, name="m8a", display_name="M8A", capability=ModelConfig.Capability.IMAGE)
|
||||
self.m2 = ModelConfig.objects.create(provider=self.prov, name="m8b", display_name="M8B", capability=ModelConfig.Capability.IMAGE)
|
||||
self.ac = APIClient()
|
||||
self.ac.force_authenticate(self.admin)
|
||||
self.nc = APIClient()
|
||||
self.nc.force_authenticate(self.normal)
|
||||
|
||||
def test_providers_list_permission_and_api_key_hidden(self):
|
||||
self.assertEqual(self.nc.get("/api/admin/providers/").status_code, 403)
|
||||
r = self.ac.get("/api/admin/providers/")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
row = next(p for p in r.data if p["name"] == "prov8")
|
||||
self.assertNotIn("api_key", row) # 密钥不回传
|
||||
self.assertTrue(row["has_api_key"])
|
||||
self.assertEqual(row["model_count"], 2)
|
||||
|
||||
def test_provider_create_key_writeonly_persisted(self):
|
||||
from apps.ai.models import ModelProvider
|
||||
|
||||
r = self.ac.post("/api/admin/providers/", {"name": "newp8", "display_name": "New", "api_key": "k123"}, format="json")
|
||||
self.assertEqual(r.status_code, 201)
|
||||
self.assertTrue(r.data["has_api_key"])
|
||||
self.assertNotIn("api_key", r.data)
|
||||
self.assertEqual(ModelProvider.objects.get(name="newp8").api_key, "k123")
|
||||
|
||||
def test_provider_toggle_status_and_delete(self):
|
||||
r = self.ac.patch(f"/api/admin/providers/{self.prov.id}/", {"status": "disabled"}, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data["status"], "disabled")
|
||||
|
||||
def test_models_list_filter(self):
|
||||
r = self.ac.get(f"/api/admin/models/?provider={self.prov.id}")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(len(r.data), 2)
|
||||
cap = self.ac.get("/api/admin/models/?capability=image")
|
||||
self.assertTrue(all(m["capability"] == "image" for m in cap.data))
|
||||
|
||||
def test_model_create_update_delete(self):
|
||||
c = self.ac.post(
|
||||
"/api/admin/models/",
|
||||
{"provider": str(self.prov.id), "name": "m8c", "display_name": "C", "capability": "text"},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(c.status_code, 201)
|
||||
mid = c.data["id"]
|
||||
up = self.ac.patch(f"/api/admin/models/{mid}/", {"unit_price": "2.5", "status": "disabled"}, format="json")
|
||||
self.assertEqual(up.data["unit_price"], "2.5000")
|
||||
self.assertEqual(up.data["status"], "disabled")
|
||||
self.assertEqual(self.ac.delete(f"/api/admin/models/{mid}/").status_code, 204)
|
||||
|
||||
def test_set_default_changes_get_default_model(self):
|
||||
from apps.ai.services import get_default_model
|
||||
|
||||
r = self.ac.post(f"/api/admin/models/{self.m2.id}/set-default/")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(r.data["is_default"])
|
||||
self.assertEqual(get_default_model("image").id, self.m2.id)
|
||||
# 改设 m1 → m2 清默认
|
||||
self.ac.post(f"/api/admin/models/{self.m1.id}/set-default/")
|
||||
self.assertEqual(get_default_model("image").id, self.m1.id)
|
||||
self.m2.refresh_from_db()
|
||||
self.assertFalse(self.m2.is_default)
|
||||
self.assertTrue(AdminAuditLog.objects.filter(action="model.set_default").exists())
|
||||
|
||||
def test_write_requires_admin(self):
|
||||
self.assertEqual(self.nc.post("/api/admin/providers/", {"name": "x", "display_name": "x"}, format="json").status_code, 403)
|
||||
self.assertEqual(self.nc.post(f"/api/admin/models/{self.m1.id}/set-default/").status_code, 403)
|
||||
|
||||
Reference in New Issue
Block a user