feat(*): 创作配置 - 支持 Midjourney、多图保存与自定义公告

Co-authored-by: ddcat666 <26564566+ddcat666@users.noreply.github.com>
This commit is contained in:
DAO-QINandddcat666 authored and ddcat666 committed 2026-10-04 10:43:31 +08:00
1 parent 002a45f0c3
commit 853c7d2ce7
107 files changed
+9261 -408

No files matched your search

+43 -1
View File
@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test";
import { defaultModelCapabilityConfig } from "../src/lib/model-capabilities";
import type { ModelProtocolDefinition } from "../src/lib/model-protocols";
import { changeChannelModelCapability, editorSectionForField, initialChannelModelValues, validateChannelModelPrices, validateChannelModelProtocol } from "../src/pages/admin/components/channel-model-editor-form";
import { changeChannelModelCapability, editorSectionForField, initialChannelModelValues, updateChannelModelUpstreamCapabilities, validateChannelModelPrices, validateChannelModelProtocol } from "../src/pages/admin/components/channel-model-editor-form";
import { defaultPriceTier } from "../src/pages/admin/components/channel-model-price-tier-form";
const definition = (value: string, capability: ModelProtocolDefinition["capability"], enabled = true): ModelProtocolDefinition => ({ value, capability, enabled, label: value, create: "POST /test", contentType: "application/json", media: "url" });
@@ -43,6 +43,48 @@ describe("channel model editor drafts", () => {
const draft = initialChannelModelValues(null, protocols);
expect(changeChannelModelCapability({ ...draft, capability: "audio" }, protocols).capabilityConfig).toBeUndefined();
});
test("switching capability to a Midjourney protocol uses its image defaults without changing prices", () => {
const draft = initialChannelModelValues(null, protocols);
const next = changeChannelModelCapability({ ...draft, capability: "image", protocol: "cangyuan-midjourney-v7", modelKey: "midjourney-v7" }, [...protocols, definition("cangyuan-midjourney-v7", "image")]);
expect(next.capabilityConfig?.image).toMatchObject({ references: { promptMaxChars: 4000, maxImages: 5, maskSupported: false }, maxOutputs: 1 });
expect(next.priceTiers[0].unitPrice).toBe(0);
});
test("Kacang image protocol selection fills capabilities without changing prices", () => {
for (const protocol of ["kacang-midjourney-special", "kacang-midjourney-v7", "kacang-midjourney"]) {
const draft = initialChannelModelValues(null, protocols);
draft.priceTiers[0].unitPrice = 12;
const next = changeChannelModelCapability({ ...draft, capability: "image", protocol }, [definition(protocol, "image")]);
expect(next.protocol).toBe(protocol);
expect(next.capabilityConfig?.image?.maxOutputs).toBe(1);
expect(next.capabilityConfig?.image?.references.maxImages).toBe(protocol === "kacang-midjourney" ? 5 : 1);
expect(next.priceTiers[0].unitPrice).toBe(12);
expect(updateChannelModelUpstreamCapabilities(next)).toBe(next);
}
});
test("V8.2 upstream changes only retier presets and preserve administrator settings", () => {
const draft = initialChannelModelValues(null, protocols);
const capabilityConfig = defaultModelCapabilityConfig("cangyuan-midjourney-v82", "midjourney-1k");
capabilityConfig.image!.references.promptMaxChars = 12000;
capabilityConfig.image!.size.default = "6:11";
const next = updateChannelModelUpstreamCapabilities({ ...draft, capability: "image", protocol: "cangyuan-midjourney-v82", modelKey: "midjourney-1k", providerModelKey: "midjourney-2k", capabilityConfig });
expect(new Set(next.capabilityConfig!.image!.size.presets!.map((preset) => preset.tier))).toEqual(new Set(["2k"]));
expect(next.capabilityConfig!.image!.size.values).toEqual(capabilityConfig.image!.size.values);
expect(next.capabilityConfig!.image!.size.default).toBe("6:11");
expect(next.capabilityConfig!.image!.references.promptMaxChars).toBe(12000);
expect(next.priceTiers).toBe(draft.priceTiers);
expect(capabilityConfig.image!.size.presets![0].tier).toBe("1k");
expect(updateChannelModelUpstreamCapabilities(next)).toBe(next);
const back = updateChannelModelUpstreamCapabilities({ ...next, providerModelKey: "midjourney-1k" });
expect(back.capabilityConfig!.image!.size.presets![0].tier).toBe("1k");
});
test("existing model profiles retain saved custom capabilities on reopening", () => {
const capabilityConfig = defaultModelCapabilityConfig("cangyuan-midjourney-v7", "midjourney-v7");
capabilityConfig.image!.references.maxImages = 2;
const item = { modelKey: "midjourney-v7", providerModelKey: "midjourney-v7", capability: "image" as const, protocol: "cangyuan-midjourney-v7", capabilityConfig, enabled: true };
const reopened = initialChannelModelValues(item as Parameters<typeof initialChannelModelValues>[0], [definition("cangyuan-midjourney-v7", "image")]);
expect(reopened.capabilityConfig?.image?.references.maxImages).toBe(2);
expect(updateChannelModelUpstreamCapabilities(reopened)).toBe(reopened);
});
test("validation errors map to their mounted tab", () => {
expect(editorSectionForField(["priceTiers", 2, "unitPrice"])).toBe("pricing");
expect(editorSectionForField(["capabilityConfig"])).toBe("capabilities");