feat(features): 功能开放 - 增加自定义渠道开关
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- 管理后台“功能开放”新增自定义渠道开关,关闭后保留用户已有配置,但隐藏配置入口、过滤自定义模型,并拦截模型目录拉取、渠道中转与新的异步生成任务。
|
||||
- 阿里云 OSS 与腾讯云 COS 的平台、个人存储支持配置可选 CDN 加速域名,上传继续使用 Endpoint,下载与预览在配置后改用 CDN 域名。
|
||||
|
||||
## v1.0.57
|
||||
|
||||
@@ -17,6 +17,10 @@ func RegisterChannelModelRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
if err := svc.RequireFeature(service.FeatureCustomChannels); err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
if !enforceRateLimit(c, "channel-models:"+user.ID, 30, time.Minute) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ func RegisterCustomRelayRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
if err := svc.RequireFeature(service.FeatureCustomChannels); err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
policy, available := loadRuntimePolicy(c, svc)
|
||||
if !available || !enforceRateLimit(c, "custom-relay:"+user.ID, policy.Request.CustomRelayPerMinute, time.Minute) {
|
||||
return
|
||||
|
||||
@@ -42,8 +42,13 @@ func RegisterFeatureAvailabilityRoutes(r *gin.RouterGroup, svc *service.Service)
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
current, err := svc.AdminFeatureAvailability(user)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 16<<10)
|
||||
var req service.FeatureAvailability
|
||||
req := current.FeatureAvailability
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
fail(c, http.StatusBadRequest, err)
|
||||
return
|
||||
|
||||
@@ -67,7 +67,7 @@ func TestRuntimeFeatureAvailabilityExposesDesktopCapabilityReadOnly(t *testing.T
|
||||
if !features.DesktopLocalChannelsEnabled {
|
||||
t.Fatal("public feature payload must expose the server-held desktop capability")
|
||||
}
|
||||
if writable := defaultFeatureAvailability(); writable.ShortDramaEnabled != features.ShortDramaEnabled || writable.TaskCenterEnabled != features.TaskCenterEnabled || writable.CreditsEnabled != features.CreditsEnabled {
|
||||
if writable := defaultFeatureAvailability(); writable.ShortDramaEnabled != features.ShortDramaEnabled || writable.TaskCenterEnabled != features.TaskCenterEnabled || writable.CreditsEnabled != features.CreditsEnabled || writable.CustomChannelsEnabled != features.CustomChannelsEnabled {
|
||||
t.Fatal("runtime capability must not alter writable feature availability")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,17 @@ import (
|
||||
const featureAvailabilitySettingKey = "feature_availability"
|
||||
|
||||
const (
|
||||
FeatureShortDrama = "shortDrama"
|
||||
FeatureTaskCenter = "taskCenter"
|
||||
FeatureCredits = "credits"
|
||||
FeatureShortDrama = "shortDrama"
|
||||
FeatureTaskCenter = "taskCenter"
|
||||
FeatureCredits = "credits"
|
||||
FeatureCustomChannels = "customChannels"
|
||||
)
|
||||
|
||||
type FeatureAvailability struct {
|
||||
ShortDramaEnabled bool `json:"shortDramaEnabled"`
|
||||
TaskCenterEnabled bool `json:"taskCenterEnabled"`
|
||||
CreditsEnabled bool `json:"creditsEnabled"`
|
||||
ShortDramaEnabled bool `json:"shortDramaEnabled"`
|
||||
TaskCenterEnabled bool `json:"taskCenterEnabled"`
|
||||
CreditsEnabled bool `json:"creditsEnabled"`
|
||||
CustomChannelsEnabled bool `json:"customChannelsEnabled"`
|
||||
}
|
||||
|
||||
type PublicFeatureAvailability struct {
|
||||
@@ -35,7 +37,7 @@ type PublicFeatureAvailability struct {
|
||||
|
||||
func defaultFeatureAvailability() FeatureAvailability {
|
||||
// 缺少配置代表尚未由运维接管,默认保持现有功能全部开放。
|
||||
return FeatureAvailability{ShortDramaEnabled: true, TaskCenterEnabled: true, CreditsEnabled: true}
|
||||
return FeatureAvailability{ShortDramaEnabled: true, TaskCenterEnabled: true, CreditsEnabled: true, CustomChannelsEnabled: true}
|
||||
}
|
||||
|
||||
func (s *Service) FeatureAvailability() (*PublicFeatureAvailability, error) {
|
||||
@@ -90,6 +92,8 @@ func (s *Service) FeatureEnabled(feature string) (bool, error) {
|
||||
return value.TaskCenterEnabled, nil
|
||||
case FeatureCredits:
|
||||
return value.CreditsEnabled, nil
|
||||
case FeatureCustomChannels:
|
||||
return value.CustomChannelsEnabled, nil
|
||||
default:
|
||||
return false, errors.New("未知功能开放配置")
|
||||
}
|
||||
@@ -110,6 +114,8 @@ func (s *Service) RequireFeature(feature string) error {
|
||||
return Forbidden("任务中心暂未开放")
|
||||
case FeatureCredits:
|
||||
return Forbidden("积分功能暂未开放")
|
||||
case FeatureCustomChannels:
|
||||
return Forbidden("自定义渠道暂未开放")
|
||||
default:
|
||||
return Forbidden("该功能暂未开放")
|
||||
}
|
||||
@@ -123,7 +129,8 @@ func (s *Service) readFeatureAvailability() (*model.SystemSetting, FeatureAvaila
|
||||
if err != nil {
|
||||
return nil, FeatureAvailability{}, err
|
||||
}
|
||||
value := FeatureAvailability{}
|
||||
// 以全开放默认值为基底,避免已有三字段配置在升级后因缺少新字段而意外关闭功能。
|
||||
value := defaultFeatureAvailability()
|
||||
if strings.TrimSpace(setting.ValueJSON) == "" || json.Unmarshal([]byte(setting.ValueJSON), &value) != nil {
|
||||
return nil, FeatureAvailability{}, errors.New("功能开放配置格式无效")
|
||||
}
|
||||
|
||||
@@ -18,7 +18,23 @@ func TestFeatureAvailabilityDefaultsToEnabled(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if setting.Configured || !setting.ShortDramaEnabled || !setting.TaskCenterEnabled || !setting.CreditsEnabled {
|
||||
if setting.Configured || !setting.ShortDramaEnabled || !setting.TaskCenterEnabled || !setting.CreditsEnabled || !setting.CustomChannelsEnabled {
|
||||
t.Fatalf("FeatureAvailability() = %#v", setting)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeatureAvailabilityLegacySettingKeepsCustomChannelsEnabled(t *testing.T) {
|
||||
svc, db := newFeatureAvailabilityTestService(t)
|
||||
legacy := &model.SystemSetting{Key: featureAvailabilitySettingKey, ValueJSON: `{"shortDramaEnabled":false,"taskCenterEnabled":true,"creditsEnabled":true}`}
|
||||
if err := db.Create(legacy).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
setting, err := svc.FeatureAvailability()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if setting.ShortDramaEnabled || !setting.CustomChannelsEnabled {
|
||||
t.Fatalf("FeatureAvailability() = %#v", setting)
|
||||
}
|
||||
}
|
||||
@@ -26,13 +42,13 @@ func TestFeatureAvailabilityDefaultsToEnabled(t *testing.T) {
|
||||
func TestUpdateFeatureAvailabilityPersistsAndAudits(t *testing.T) {
|
||||
svc, db := newFeatureAvailabilityTestService(t)
|
||||
actor := &model.User{ID: "admin-1", Role: model.UserRoleAdmin}
|
||||
want := FeatureAvailability{ShortDramaEnabled: false, TaskCenterEnabled: true, CreditsEnabled: false}
|
||||
want := FeatureAvailability{ShortDramaEnabled: false, TaskCenterEnabled: true, CreditsEnabled: false, CustomChannelsEnabled: true}
|
||||
|
||||
setting, err := svc.UpdateFeatureAvailability(actor, want)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !setting.Configured || setting.ShortDramaEnabled || !setting.TaskCenterEnabled || setting.CreditsEnabled {
|
||||
if !setting.Configured || setting.ShortDramaEnabled || !setting.TaskCenterEnabled || setting.CreditsEnabled || !setting.CustomChannelsEnabled {
|
||||
t.Fatalf("UpdateFeatureAvailability() = %#v", setting)
|
||||
}
|
||||
if err := svc.RequireFeature(FeatureShortDrama); err == nil {
|
||||
@@ -52,10 +68,33 @@ func TestUpdateFeatureAvailabilityPersistsAndAudits(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomChannelTaskInputRequiresFeature(t *testing.T) {
|
||||
svc, _ := newFeatureAvailabilityTestService(t)
|
||||
actor := &model.User{ID: "admin-1", Role: model.UserRoleAdmin}
|
||||
if _, err := svc.UpdateFeatureAvailability(actor, FeatureAvailability{ShortDramaEnabled: true, TaskCenterEnabled: true, CreditsEnabled: true, CustomChannelsEnabled: false}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
customInput, err := normalizeTaskInput(map[string]any{"config": providerConfig{BaseURL: "https://example.com", APIKey: "private-key", Model: "text-model"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := svc.requireCustomChannelsForTaskInput(customInput); err == nil {
|
||||
t.Fatal("custom channel task must be rejected when the feature is disabled")
|
||||
}
|
||||
systemInput, err := normalizeTaskInput(map[string]any{"config": providerConfig{ChannelID: "system-1", BaseURL: "/api/ai/system/system-1", APIKey: "system", Model: "text-model"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := svc.requireCustomChannelsForTaskInput(systemInput); err != nil {
|
||||
t.Fatalf("system channel task error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskBillingOrderSkipsPricingWhenCreditsDisabled(t *testing.T) {
|
||||
svc, _ := newFeatureAvailabilityTestService(t)
|
||||
actor := &model.User{ID: "admin-1", Role: model.UserRoleAdmin}
|
||||
if _, err := svc.UpdateFeatureAvailability(actor, FeatureAvailability{ShortDramaEnabled: true, TaskCenterEnabled: true, CreditsEnabled: false}); err != nil {
|
||||
if _, err := svc.UpdateFeatureAvailability(actor, FeatureAvailability{ShortDramaEnabled: true, TaskCenterEnabled: true, CreditsEnabled: false, CustomChannelsEnabled: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -327,6 +327,9 @@ func (s *Service) CreateTask(userID string, req CreateTaskRequest) (*model.Task,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.requireCustomChannelsForTaskInput(normalizedInput); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.ValidateTaskCapability(normalizedInput); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -398,6 +401,24 @@ func normalizeTaskInput(input map[string]any) (map[string]any, error) {
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func (s *Service) requireCustomChannelsForTaskInput(input map[string]any) error {
|
||||
if !taskInputUsesCustomChannel(input) {
|
||||
return nil
|
||||
}
|
||||
return s.RequireFeature(FeatureCustomChannels)
|
||||
}
|
||||
|
||||
func taskInputUsesCustomChannel(input map[string]any) bool {
|
||||
config, ok := input["config"].(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
channelID, _ := config["channelId"].(string)
|
||||
baseURL, _ := config["baseUrl"].(string)
|
||||
apiKey, _ := config["apiKey"].(string)
|
||||
return strings.TrimSpace(channelID) == "" && strings.TrimSpace(baseURL) != "" && strings.TrimSpace(apiKey) != ""
|
||||
}
|
||||
|
||||
func compactPersistedValue(value interface{}) interface{} {
|
||||
switch item := value.(type) {
|
||||
case map[string]interface{}:
|
||||
@@ -507,6 +528,9 @@ func (s *Service) RetryTask(userID string, id string) (*model.Task, error) {
|
||||
if err := json.Unmarshal([]byte(decryptedInput), &billingInput); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.requireCustomChannelsForTaskInput(billingInput); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
billingOrder, err := s.taskBillingOrder(userID, task, billingInput)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -37,5 +37,5 @@ export function EmailSettingsPage() {
|
||||
}
|
||||
|
||||
export function FeatureAvailabilityPage() {
|
||||
return <AdminPageFrame title="功能开放" description="控制用户工作台入口与计费模式"><Suspense fallback={<PageFallback label="功能开放配置" />}><FeatureAvailabilityPanel /></Suspense></AdminPageFrame>;
|
||||
return <AdminPageFrame title="功能开放" description="控制用户工作台入口、渠道与计费模式"><Suspense fallback={<PageFallback label="功能开放配置" />}><FeatureAvailabilityPanel /></Suspense></AdminPageFrame>;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ const adminNavigation: Array<{ label: string; items: AdminNavigationItem[] }> =
|
||||
{
|
||||
label: "系统配置",
|
||||
items: [
|
||||
{ path: "/admin/settings/features", label: "功能开放", description: "菜单、页面与积分模式", icon: <ToggleLeft className="size-4" /> },
|
||||
{ path: "/admin/settings/features", label: "功能开放", description: "菜单、渠道与积分模式", icon: <ToggleLeft className="size-4" /> },
|
||||
{ path: "/admin/settings/drawing-engine", label: "绘图工具", description: "画布绘图节点默认引擎", icon: <Paintbrush className="size-4" /> },
|
||||
{ path: "/admin/settings/runtime-policy", label: "资源与策略", description: "配额、并发、频控与超时", icon: <Settings2 className="size-4" /> },
|
||||
{ path: "/admin/settings/access", label: "登录与注册", description: "注册策略与 Linux.do", icon: <ShieldCheck className="size-4" /> },
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { App, Switch } from "antd";
|
||||
import { Clapperboard, Coins, ListChecks, ToggleLeft } from "lucide-react";
|
||||
import { Clapperboard, Coins, ListChecks, RadioTower, ToggleLeft } from "lucide-react";
|
||||
|
||||
import { getAdminFeatureAvailability, updateAdminFeatureAvailability } from "@/services/api/auth";
|
||||
import { useUserStore, type FeatureAvailability } from "@/stores/use-user-store";
|
||||
import { SettingsSectionCard } from "./admin-ui";
|
||||
|
||||
type FeatureKey = "shortDramaEnabled" | "taskCenterEnabled" | "creditsEnabled";
|
||||
type FeatureKey = "shortDramaEnabled" | "taskCenterEnabled" | "creditsEnabled" | "customChannelsEnabled";
|
||||
|
||||
const featureRows: Array<{ key: FeatureKey; title: string; menu: string; description: string; icon: ReactNode }> = [
|
||||
{ key: "shortDramaEnabled", title: "短剧创作", menu: "/projects", description: "关闭后隐藏短剧入口,并拦截项目列表、详情和项目 API。已有项目数据不会删除。", icon: <Clapperboard className="size-4" /> },
|
||||
{ key: "taskCenterEnabled", title: "任务", menu: "/tasks", description: "关闭后仅隐藏并拦截任务中心页面;生成任务仍会创建、执行、记录和恢复。", icon: <ListChecks className="size-4" /> },
|
||||
{ key: "creditsEnabled", title: "积分中心", menu: "/wallet", description: "关闭后隐藏用户积分入口,新创建的任务和系统渠道请求不再冻结或消费积分。", icon: <Coins className="size-4" /> },
|
||||
{ key: "customChannelsEnabled", title: "自定义渠道", menu: "/settings?section=channels", description: "关闭后隐藏用户自定义渠道入口,并拦截模型目录拉取、渠道中转和新的生成任务。已有渠道配置不会删除。", icon: <RadioTower className="size-4" /> },
|
||||
];
|
||||
|
||||
export default function FeatureAvailabilityPanel() {
|
||||
@@ -41,6 +42,7 @@ export default function FeatureAvailabilityPanel() {
|
||||
shortDramaEnabled: next.shortDramaEnabled,
|
||||
taskCenterEnabled: next.taskCenterEnabled,
|
||||
creditsEnabled: next.creditsEnabled,
|
||||
customChannelsEnabled: next.customChannelsEnabled,
|
||||
});
|
||||
setFeatures(result.features);
|
||||
setGlobalFeatures(result.features);
|
||||
@@ -73,7 +75,7 @@ export default function FeatureAvailabilityPanel() {
|
||||
<SettingsSectionCard
|
||||
icon={<ToggleLeft className="size-4" />}
|
||||
title="用户功能开放"
|
||||
description="统一控制用户工作台入口、页面访问和积分消费策略。"
|
||||
description="统一控制用户工作台入口、页面访问、渠道能力和积分消费策略。"
|
||||
status={{ label: features ? `${enabledCount}/${featureRows.length} 已开放` : "读取中", color: enabledCount === featureRows.length ? "success" : "default" }}
|
||||
>
|
||||
<div className="divide-y divide-border/75">
|
||||
|
||||
@@ -34,17 +34,24 @@ export default function SettingsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const requestedSection = searchParams.get("section");
|
||||
const [activeTab, setActiveTab] = useState<ConfigSectionKey>(isConfigSection(requestedSection) ? requestedSection : "channels");
|
||||
const customChannelsEnabled = useUserStore((state) => state.features.customChannelsEnabled);
|
||||
const initialSection = isConfigSection(requestedSection) ? requestedSection : customChannelsEnabled ? "channels" : "models";
|
||||
const [activeTab, setActiveTab] = useState<ConfigSectionKey>(initialSection === "channels" && !customChannelsEnabled ? "models" : initialSection);
|
||||
const config = useConfigStore((state) => state.config);
|
||||
const effectiveConfig = useEffectiveConfig();
|
||||
const updateConfig = useConfigStore((state) => state.updateConfig);
|
||||
const shouldPromptContinue = searchParams.get("continue") === "1";
|
||||
const userId = useUserStore((state) => state.user?.id);
|
||||
const userChannels = config.channels.filter((channel) => channel.scope !== "system");
|
||||
const visibleConfigSections = customChannelsEnabled ? configSections : configSections.filter((section) => section.key !== "channels");
|
||||
|
||||
useEffect(() => {
|
||||
if (isConfigSection(requestedSection)) setActiveTab(requestedSection);
|
||||
}, [requestedSection]);
|
||||
if (isConfigSection(requestedSection) && (requestedSection !== "channels" || customChannelsEnabled)) {
|
||||
setActiveTab(requestedSection);
|
||||
return;
|
||||
}
|
||||
if (!customChannelsEnabled) setActiveTab((current) => (current === "channels" ? "models" : current));
|
||||
}, [customChannelsEnabled, requestedSection]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
@@ -65,7 +72,7 @@ export default function SettingsPage() {
|
||||
};
|
||||
|
||||
const finishConfig = () => {
|
||||
const invalidChannel = userChannels.find((channel) => channelValidationError(channel));
|
||||
const invalidChannel = customChannelsEnabled ? userChannels.find((channel) => channelValidationError(channel)) : undefined;
|
||||
if (invalidChannel) {
|
||||
selectSection("channels");
|
||||
message.warning(`${invalidChannel.name || "未命名渠道"}:${channelValidationError(invalidChannel)}`);
|
||||
@@ -73,9 +80,9 @@ export default function SettingsPage() {
|
||||
return;
|
||||
}
|
||||
const hasReadyLocalRuntime = effectiveConfig.channels.some((channel) => channel.transport === "local-runtime" && channel.enabled !== false && Boolean(channel.localModels?.length));
|
||||
if (!config.channels.some(isChannelReady) && !hasReadyLocalRuntime) {
|
||||
selectSection("channels");
|
||||
message.error(shouldPromptContinue ? "请先完成至少一个渠道的 Base URL、API Key 和模型配置" : "当前没有可用渠道,请先完成连接信息和模型配置");
|
||||
if (!effectiveConfig.channels.some(isChannelReady) && !hasReadyLocalRuntime) {
|
||||
selectSection(customChannelsEnabled ? "channels" : "models");
|
||||
message.error(customChannelsEnabled ? (shouldPromptContinue ? "请先完成至少一个渠道的 Base URL、API Key 和模型配置" : "当前没有可用渠道,请先完成连接信息和模型配置") : "当前没有可用的系统模型,请联系管理员配置系统渠道");
|
||||
return;
|
||||
}
|
||||
message.success("配置已保存,正在返回创作页面");
|
||||
@@ -190,7 +197,7 @@ export default function SettingsPage() {
|
||||
<div className="settings-library-frame flex min-h-0 flex-1 flex-col md:flex-row">
|
||||
<aside className="settings-nav-panel w-full shrink-0 md:w-[200px]">
|
||||
<nav className="thin-scrollbar flex gap-1 overflow-x-auto p-2 md:block md:space-y-1 md:p-2.5" aria-label="配置分类">
|
||||
{configSections.map((item) => {
|
||||
{visibleConfigSections.map((item) => {
|
||||
const selected = item.key === activeTab;
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -351,7 +351,7 @@ export function getAdminFeatureAvailability() {
|
||||
return request<{ features: FeatureAvailability }>(api.get("/admin/settings/features"));
|
||||
}
|
||||
|
||||
export function updateAdminFeatureAvailability(features: Pick<FeatureAvailability, "shortDramaEnabled" | "taskCenterEnabled" | "creditsEnabled">) {
|
||||
export function updateAdminFeatureAvailability(features: Pick<FeatureAvailability, "shortDramaEnabled" | "taskCenterEnabled" | "creditsEnabled" | "customChannelsEnabled">) {
|
||||
return request<{ features: FeatureAvailability }>(api.patch("/admin/settings/features", features));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { modelProtocolCapability, normalizeModelProtocol, type ModelProtocol } f
|
||||
import { normalizeVideoDuration, normalizeVideoResolution } from "@/lib/video-generation-options";
|
||||
import type { ModelCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import { useLocalDreaminaModelStore } from "@/stores/use-local-dreamina-model-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
import type { DreaminaLocalModel } from "@/services/local-dreamina-model-catalog";
|
||||
|
||||
export type ApiCallFormat = "openai" | "gemini";
|
||||
@@ -345,9 +346,19 @@ function normalizeSelectedModel(value: string, channels: ModelChannel[], options
|
||||
|
||||
export function useEffectiveConfig() {
|
||||
const config = useConfigStore((state) => state.config);
|
||||
const customChannelsEnabled = useUserStore((state) => state.features.customChannelsEnabled);
|
||||
const catalogState = useLocalDreaminaModelStore((state) => state.state);
|
||||
const dreaminaModels = useLocalDreaminaModelStore((state) => state.models);
|
||||
return useMemo(() => effectiveConfigWithDreamina(config, catalogState, dreaminaModels), [catalogState, config, dreaminaModels]);
|
||||
return useMemo(
|
||||
() => effectiveConfigWithDreamina(effectiveConfigForCustomChannels(config, customChannelsEnabled), catalogState, dreaminaModels),
|
||||
[catalogState, config, customChannelsEnabled, dreaminaModels],
|
||||
);
|
||||
}
|
||||
|
||||
export function effectiveConfigForCustomChannels(config: AiConfig, customChannelsEnabled: boolean): AiConfig {
|
||||
if (customChannelsEnabled) return config;
|
||||
const channels = config.channels.filter((channel) => channel.scope === "system" || channel.transport === "local-runtime");
|
||||
return normalizeConfigSnapshot({ config: { ...config, channels } }).config;
|
||||
}
|
||||
|
||||
export function effectiveConfigWithDreamina(config: AiConfig, catalogState: "idle" | "loading" | "ready" | "error", dreaminaModels: DreaminaLocalModel[]): AiConfig {
|
||||
|
||||
@@ -28,6 +28,7 @@ export type FeatureAvailability = {
|
||||
shortDramaEnabled: boolean;
|
||||
taskCenterEnabled: boolean;
|
||||
creditsEnabled: boolean;
|
||||
customChannelsEnabled: boolean;
|
||||
desktopLocalChannelsEnabled: boolean;
|
||||
configured?: boolean;
|
||||
updatedBy?: string;
|
||||
@@ -38,6 +39,7 @@ export const defaultFeatureAvailability: FeatureAvailability = {
|
||||
shortDramaEnabled: true,
|
||||
taskCenterEnabled: true,
|
||||
creditsEnabled: true,
|
||||
customChannelsEnabled: true,
|
||||
desktopLocalChannelsEnabled: false,
|
||||
};
|
||||
|
||||
@@ -64,7 +66,7 @@ export const useUserStore = create<UserStore>()((set) => ({
|
||||
setUser: (user) => set({ user }),
|
||||
setRuntimeLimits: (runtimeLimits) => set({ runtimeLimits: runtimeLimits || { activeTaskLimit: 5, resourceUploadMB: 50, sessionUploadMB: 32 } }),
|
||||
setDrawingEngine: (drawingEngine) => set({ drawingEngine: drawingEngine || { defaultEngine: DEFAULT_DRAWING_ENGINE } }),
|
||||
setFeatures: (features) => set({ features: features || defaultFeatureAvailability }),
|
||||
setFeatures: (features) => set({ features: features ? { ...defaultFeatureAvailability, ...features } : defaultFeatureAvailability }),
|
||||
setHydrated: (hydrated) => set({ hydrated }),
|
||||
clearSession: () => set({ user: null, runtimeLimits: { activeTaskLimit: 5, resourceUploadMB: 50, sessionUploadMB: 32 }, drawingEngine: { defaultEngine: DEFAULT_DRAWING_ENGINE }, features: defaultFeatureAvailability }),
|
||||
}));
|
||||
|
||||
@@ -213,6 +213,24 @@ test("effective config projects an asynchronously arriving Dreamina catalog with
|
||||
expect(defaultConfig.channels.some((channel) => channel.id === "local:dreamina-cli")).toBe(false);
|
||||
});
|
||||
|
||||
test("effective config removes custom channels when administrators disable them", async () => {
|
||||
const { createModelChannel, effectiveConfigForCustomChannels, normalizeConfigSnapshot } = await import("../src/stores/use-config-store");
|
||||
const config = normalizeConfigSnapshot({
|
||||
config: {
|
||||
channels: [
|
||||
createModelChannel({ id: "system-1", scope: "system", name: "系统渠道", baseUrl: "/api/ai/system/system-1", apiKey: "system", models: ["system-model"], modelCosts: [{ model: "system-model", capability: "text", billingMode: "fixed_request", unitPriceMicrocredits: 0 }] }),
|
||||
createModelChannel({ id: "custom-1", scope: "user", name: "自定义渠道", baseUrl: "https://example.com", apiKey: "private-key", models: ["custom-model"] }),
|
||||
],
|
||||
},
|
||||
}).config;
|
||||
|
||||
const effective = effectiveConfigForCustomChannels(config, false);
|
||||
expect(effective.channels.map((channel) => channel.id)).toEqual(["system-1"]);
|
||||
expect(effective.models).toContain("system-1::system-model");
|
||||
expect(effective.models).not.toContain("custom-1::custom-model");
|
||||
expect(config.channels.map((channel) => channel.id)).toEqual(["system-1", "custom-1"]);
|
||||
});
|
||||
|
||||
function catalogResponse() {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
|
||||
Reference in New Issue
Block a user