大量优化修改扣积分规则

This commit is contained in:
Azmat@qq.com
2026-09-04 17:20:22 +08:00
parent 3e904479d9
commit 15718a60bf
41 changed files with 1590 additions and 429 deletions
+212 -14
View File
@@ -7,6 +7,18 @@ import { CustomSelect } from "../../components/custom-select";
import type { AdminModel, AdminProvider } from "../../types";
import { pts } from "../stage-config";
function sortAdminModels(list: AdminModel[]): AdminModel[] {
// 默认最前,启用次之,停用靠后;同档按能力、供应商
return [...list].sort((a, b) => {
if (Boolean(a.is_default) !== Boolean(b.is_default)) return a.is_default ? -1 : 1;
if ((a.status === "active") !== (b.status === "active")) return a.status === "active" ? -1 : 1;
const cap = String(a.capability || "").localeCompare(String(b.capability || ""));
if (cap !== 0) return cap;
return String(a.provider_name || a.provider || "").localeCompare(String(b.provider_name || b.provider || ""));
});
}
type Notify = (type: "success" | "error" | "info", text: string) => void;
const PAGE_SIZE = 10;
@@ -20,7 +32,100 @@ function statusPill(active: boolean) {
}
const EMPTY_PROVIDER = { id: "", name: "", display_name: "", base_url: "", api_key: "", status: "active" };
const EMPTY_MODEL = { id: "", provider: "", name: "", display_name: "", capability: "text", endpoint: "", unit_price: "", status: "active" };
const VIDEO_RES_OPTIONS = ["480p", "720p", "1080p", "4k"];
type VideoTier = { resolution: string; points_per_second: string; points_per_second_with_ref: string };
type ModelForm = {
id: string;
provider: string;
name: string;
display_name: string;
capability: string;
endpoint: string;
unit_price: string;
status: string;
durationsText: string;
videoTiers: VideoTier[];
};
const EMPTY_MODEL: ModelForm = {
id: "", provider: "", name: "", display_name: "", capability: "text", endpoint: "",
unit_price: "", status: "active", durationsText: "4,5,6,8,10,12,15",
videoTiers: [{ resolution: "480p", points_per_second: "20", points_per_second_with_ref: "" }],
};
function readModelForm(m: AdminModel): ModelForm {
const meta = (m.metadata || {}) as Record<string, unknown>;
const caps = (meta.capabilities || {}) as Record<string, unknown>;
const pricing = (meta.points_pricing || {}) as Record<string, unknown>;
const durations = Array.isArray(caps.durations) ? caps.durations.map(String) : [];
const tiersRaw = Array.isArray(pricing.tiers) ? pricing.tiers : [];
const videoTiers: VideoTier[] = tiersRaw
.filter((row): row is Record<string, unknown> => !!row && typeof row === "object")
.map((row) => ({
resolution: String(row.resolution || ""),
points_per_second: String(row.points_per_second ?? ""),
points_per_second_with_ref: String(row.points_per_second_with_ref ?? ""),
}));
let unit = m.unit_price || "";
if (m.capability === "image" && pricing.points_per_image != null) unit = String(pricing.points_per_image);
if ((m.capability === "text" || m.capability === "vision") && pricing.points_per_call != null) {
unit = String(pricing.points_per_call);
}
return {
id: m.id,
provider: m.provider,
name: m.name,
display_name: m.display_name,
capability: m.capability,
endpoint: m.endpoint,
unit_price: unit,
status: m.status,
durationsText: durations.length ? durations.join(",") : "4,5,6,8,10,12,15",
videoTiers: videoTiers.length
? videoTiers
: [{ resolution: "480p", points_per_second: "", points_per_second_with_ref: "" }],
};
}
function buildMetadata(form: ModelForm): Record<string, unknown> {
if (form.capability === "image") {
const pts = Number(form.unit_price || 0);
return { points_pricing: { mode: "per_image", points_per_image: Number.isFinite(pts) ? pts : 0 } };
}
if (form.capability === "text" || form.capability === "vision") {
const pts = Number(form.unit_price || 0);
return { points_pricing: { mode: "per_call", points_per_call: Number.isFinite(pts) ? pts : 0 } };
}
if (form.capability === "video") {
const durations = form.durationsText
.split(/[,,\s]+/)
.map((x) => Number(x))
.filter((n) => Number.isFinite(n) && n > 0);
const tiers = form.videoTiers
.filter((row) => row.resolution && row.points_per_second !== "")
.map((row) => {
const item: Record<string, unknown> = {
resolution: row.resolution,
points_per_second: Number(row.points_per_second),
};
if (row.points_per_second_with_ref !== "") {
item.points_per_second_with_ref = Number(row.points_per_second_with_ref);
}
return item;
});
return {
capabilities: {
resolutions: tiers.map((t) => String(t.resolution)),
durations,
operations: ["video_generate"],
},
points_pricing: { mode: "per_second", tiers },
};
}
return {};
}
export function AdminModelsPage({ notify }: { notify: Notify }) {
const [providers, setProviders] = useState<AdminProvider[]>([]);
@@ -28,7 +133,7 @@ export function AdminModelsPage({ notify }: { notify: Notify }) {
const [modelPage, setModelPage] = useState(1);
const [loading, setLoading] = useState(true);
const [provModal, setProvModal] = useState<typeof EMPTY_PROVIDER | null>(null);
const [modelModal, setModelModal] = useState<typeof EMPTY_MODEL | null>(null);
const [modelModal, setModelModal] = useState<ModelForm | null>(null);
const [saving, setSaving] = useState(false);
const load = useCallback(async () => {
@@ -36,7 +141,7 @@ export function AdminModelsPage({ notify }: { notify: Notify }) {
try {
const [ps, ms] = await Promise.all([adminApi.providers(), adminApi.models()]);
setProviders(ps);
setModels(ms);
setModels(sortAdminModels(ms));
} catch {
notify("error", "加载模型供应商失败");
} finally {
@@ -101,12 +206,32 @@ export function AdminModelsPage({ notify }: { notify: Notify }) {
async function saveModel() {
if (!modelModal || saving) return;
if (!modelModal.provider || !modelModal.name.trim()) { notify("error", "请选择供应商并填写模型名"); return; }
if (modelModal.capability === "video") {
const ok = modelModal.videoTiers.some((row) => row.resolution && row.points_per_second !== "");
if (!ok) { notify("error", "视频模型请至少添加一档分辨率和每秒积分"); return; }
}
setSaving(true);
try {
const metadata = buildMetadata(modelModal);
if (modelModal.id) {
await adminApi.updateModel(modelModal.id, { display_name: modelModal.display_name, endpoint: modelModal.endpoint, unit_price: modelModal.unit_price || "0", status: modelModal.status });
await adminApi.updateModel(modelModal.id, {
display_name: modelModal.display_name,
endpoint: modelModal.endpoint,
unit_price: modelModal.unit_price || "0",
status: modelModal.status,
metadata,
});
} else {
await adminApi.createModel({ provider: modelModal.provider, name: modelModal.name, display_name: modelModal.display_name || modelModal.name, capability: modelModal.capability, endpoint: modelModal.endpoint, unit_price: modelModal.unit_price || "0", status: modelModal.status });
await adminApi.createModel({
provider: modelModal.provider,
name: modelModal.name,
display_name: modelModal.display_name || modelModal.name,
capability: modelModal.capability,
endpoint: modelModal.endpoint,
unit_price: modelModal.unit_price || "0",
status: modelModal.status,
metadata,
} as Parameters<typeof adminApi.createModel>[0] & { metadata: Record<string, unknown> });
}
notify("success", "模型已保存");
setModelModal(null);
@@ -170,7 +295,7 @@ export function AdminModelsPage({ notify }: { notify: Notify }) {
<td>{m.is_default ? <span className="pill info"><span className="dot" />默认</span> : <span className="muted">—</span>}</td>
<td className="col-actions">
{!m.is_default && <button className="btn btn-sm btn-ghost" type="button" onClick={() => setDefault(m)}>设默认</button>}
<button className="btn btn-sm btn-ghost" type="button" onClick={() => setModelModal({ id: m.id, provider: m.provider, name: m.name, display_name: m.display_name, capability: m.capability, endpoint: m.endpoint, unit_price: m.unit_price, status: m.status })}>编辑</button>
<button className="btn btn-sm btn-ghost" type="button" onClick={() => setModelModal(readModelForm(m))}>编辑</button>
<button className={`btn btn-sm btn-ghost${m.status === "active" ? " danger" : ""}`} type="button" onClick={() => toggleModel(m)}>{m.status === "active" ? "停用" : "启用"}</button>
<button className="btn btn-sm btn-ghost danger" type="button" onClick={() => delModel(m)}>删除</button>
</td>
@@ -184,7 +309,7 @@ export function AdminModelsPage({ notify }: { notify: Notify }) {
)}
{provModal && (
<div className="modal-bg show" onClick={(e) => { if (e.target === e.currentTarget) setProvModal(null); }}>
<div className="modal-bg show">
<div className="modal" role="dialog" aria-modal="true" aria-label="供应商">
<span className="corner-tr">+</span><span className="corner-bl">+</span>
<div className="modal-h">
@@ -219,7 +344,7 @@ export function AdminModelsPage({ notify }: { notify: Notify }) {
)}
{modelModal && (
<div className="modal-bg show" onClick={(e) => { if (e.target === e.currentTarget) setModelModal(null); }}>
<div className="modal-bg show">
<div className="modal" role="dialog" aria-modal="true" aria-label="模型">
<span className="corner-tr">+</span><span className="corner-bl">+</span>
<div className="modal-h">
@@ -259,16 +384,89 @@ export function AdminModelsPage({ notify }: { notify: Notify }) {
<label className="field-label">显示名</label>
<input className="input" type="text" value={modelModal.display_name} onChange={(e) => setModelModal((m) => m && ({ ...m, display_name: e.target.value }))} />
</div>
<div className="field-row">
<div className="field">
<label className="field-label">endpoint</label>
<input className="input" type="text" value={modelModal.endpoint} onChange={(e) => setModelModal((m) => m && ({ ...m, endpoint: e.target.value }))} />
</div>
{modelModal.capability === "image" ? (
<div className="field">
<label className="field-label">积分规则 · 每张</label>
<input className="input" type="number" min={0} step={1} placeholder="例如 10" value={modelModal.unit_price} onChange={(e) => setModelModal((m) => m && ({ ...m, unit_price: e.target.value }))} />
<div className="field-hint">老板填写:用该模型出 1 张图扣多少积分</div>
</div>
) : null}
{modelModal.capability === "text" || modelModal.capability === "vision" ? (
<div className="field">
<label className="field-label">积分规则 · 每次</label>
<input className="input" type="number" min={0} step={1} placeholder="例如 4" value={modelModal.unit_price} onChange={(e) => setModelModal((m) => m && ({ ...m, unit_price: e.target.value }))} />
<div className="field-hint">老板填写:调用 1 次扣多少积分</div>
</div>
) : null}
{modelModal.capability === "video" ? (
<>
<div className="field">
<label className="field-label">可用时长(秒,逗号分隔)</label>
<input className="input" type="text" value={modelModal.durationsText} onChange={(e) => setModelModal((m) => m && ({ ...m, durationsText: e.target.value }))} />
</div>
<div className="field">
<label className="field-label">分辨率积分(每秒)</label>
<div className="field-hint">每个分辨率一档;含视频参考可另填更高积分,不填则同普通价</div>
<div style={{ display: "grid", gap: 8, marginTop: 8 }}>
{modelModal.videoTiers.map((tier, index) => (
<div key={index} className="field-row" style={{ alignItems: "end" }}>
<div className="field">
<label className="field-label">分辨率</label>
<CustomSelect
fill
value={tier.resolution}
onChange={(next) => setModelModal((m) => {
if (!m) return m;
const videoTiers = m.videoTiers.map((row, i) => i === index ? { ...row, resolution: next } : row);
return { ...m, videoTiers };
})}
options={VIDEO_RES_OPTIONS.map((r) => ({ value: r, label: r }))}
/>
</div>
<div className="field">
<label className="field-label">积分/秒</label>
<input className="input" type="number" min={0} step={1} value={tier.points_per_second} onChange={(e) => setModelModal((m) => {
if (!m) return m;
const videoTiers = m.videoTiers.map((row, i) => i === index ? { ...row, points_per_second: e.target.value } : row);
return { ...m, videoTiers };
})} />
</div>
<div className="field">
<label className="field-label">含视频参考·积分/秒</label>
<input className="input" type="number" min={0} step={1} placeholder="可选" value={tier.points_per_second_with_ref} onChange={(e) => setModelModal((m) => {
if (!m) return m;
const videoTiers = m.videoTiers.map((row, i) => i === index ? { ...row, points_per_second_with_ref: e.target.value } : row);
return { ...m, videoTiers };
})} />
</div>
<button className="btn btn-sm btn-ghost" type="button" onClick={() => setModelModal((m) => m && ({ ...m, videoTiers: m.videoTiers.filter((_, i) => i !== index) }))}>删除</button>
</div>
))}
</div>
<button
className="btn btn-sm"
type="button"
style={{ marginTop: 8 }}
onClick={() => setModelModal((m) => m && ({
...m,
videoTiers: [...m.videoTiers, { resolution: "720p", points_per_second: "", points_per_second_with_ref: "" }],
}))}
>
+ 添加分辨率
</button>
</div>
</>
) : null}
{!["image", "text", "vision", "video"].includes(modelModal.capability) ? (
<div className="field">
<label className="field-label">单价(积分/次)</label>
<input className="input" type="text" placeholder="0" value={modelModal.unit_price} onChange={(e) => setModelModal((m) => m && ({ ...m, unit_price: e.target.value }))} />
</div>
<div className="field">
<label className="field-label">endpoint</label>
<input className="input" type="text" value={modelModal.endpoint} onChange={(e) => setModelModal((m) => m && ({ ...m, endpoint: e.target.value }))} />
</div>
</div>
) : null}
</div>
<div className="modal-f">
<button className="btn" type="button" onClick={() => setModelModal(null)}>取消</button>