Commit 6298b0f3 by CaIon

fix(plugin): suppress factory layer when disabling an overridden task plugin

Disabling a task plugin that has both a factory built-in and an override
row only flipped the override flag, so the built-in kept routing the same
models and same-name uploads (e.g. minimax-h3 vs MiniMax-H3) still hit a
routing conflict. Now the key also enters the disabled-factory set and the
list reports "disabled" instead of "disabled_fallback" when nothing serves.
parent 5c7cca01
...@@ -193,6 +193,11 @@ func ListTaskPlugins(c *gin.Context) { ...@@ -193,6 +193,11 @@ func ListTaskPlugins(c *gin.Context) {
} else { } else {
item.RuntimeStatus = "not_registered" item.RuntimeStatus = "not_registered"
} }
// "disabled_fallback" promises that the built-in still serves. When
// the factory layer is suppressed as well, nothing serves this key.
if item.RuntimeStatus == "disabled_fallback" && hasFactory && setting.IsTaskPluginFactoryDisabled(key) {
item.RuntimeStatus = "disabled"
}
} else { } else {
item.Source = "factory" item.Source = "factory"
item.Meta = factoryMeta item.Meta = factoryMeta
...@@ -473,9 +478,12 @@ func SetTaskPluginStatus(c *gin.Context) { ...@@ -473,9 +478,12 @@ func SetTaskPluginStatus(c *gin.Context) {
common.ApiError(c, lookupErr) common.ApiError(c, lookupErr)
return return
} }
// The disabled set suppresses only the factory fallback layer. An enabled // Switching a key off must silence every layer that can serve it. The
// override for the same key keeps serving and is toggled independently. // factory built-in goes into the disabled set even when an override row
if taskPluginHasFactory(key) && !hasActiveOverride { // exists; otherwise the built-in would keep routing the same models (and
// blocking same-model uploads) right after the administrator disabled the
// plugin. Switching on reverses both layers.
if taskPluginHasFactory(key) {
keys := setting.GetTaskPluginDisabledFactoryKeys() keys := setting.GetTaskPluginDisabledFactoryKeys()
if *request.Enabled { if *request.Enabled {
next := make([]string, 0, len(keys)) next := make([]string, 0, len(keys))
...@@ -501,8 +509,10 @@ func SetTaskPluginStatus(c *gin.Context) { ...@@ -501,8 +509,10 @@ func SetTaskPluginStatus(c *gin.Context) {
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
common.ApiSuccess(c, gin.H{"plugin_enabled": *request.Enabled, "disabled_channels": disabledChannels}) if !hasActiveOverride {
return common.ApiSuccess(c, gin.H{"plugin_enabled": *request.Enabled, "disabled_channels": disabledChannels})
return
}
} }
if err := model.SetTaskPluginEnabled(key, *request.Enabled); err != nil { if err := model.SetTaskPluginEnabled(key, *request.Enabled); err != nil {
common.ApiError(c, err) common.ApiError(c, err)
......
...@@ -248,7 +248,7 @@ func TestDisableFactoryPluginRespectsInUseGuard(t *testing.T) { ...@@ -248,7 +248,7 @@ func TestDisableFactoryPluginRespectsInUseGuard(t *testing.T) {
assert.True(t, ok) assert.True(t, ok)
} }
func TestDisableFactoryOverrideRowKeepsEnabledFlagPath(t *testing.T) { func TestDisableFactoryOverrideRowSuppressesBothLayers(t *testing.T) {
setupTaskPluginFactoryDisableTest(t) setupTaskPluginFactoryDisableTest(t)
factorySource, err := plugins.Source("kling") factorySource, err := plugins.Source("kling")
require.NoError(t, err) require.NoError(t, err)
...@@ -267,20 +267,51 @@ func TestDisableFactoryOverrideRowKeepsEnabledFlagPath(t *testing.T) { ...@@ -267,20 +267,51 @@ func TestDisableFactoryOverrideRowKeepsEnabledFlagPath(t *testing.T) {
recorder := postTaskPluginStatus(t, "kling", "", `{"enabled":false}`) recorder := postTaskPluginStatus(t, "kling", "", `{"enabled":false}`)
assert.Contains(t, recorder.Body.String(), `"success":true`) assert.Contains(t, recorder.Body.String(), `"success":true`)
assert.Empty(t, setting.GetTaskPluginDisabledFactoryKeys()) assert.Equal(t, []string{"kling"}, setting.GetTaskPluginDisabledFactoryKeys())
row, err := model.GetTaskPluginVersion("kling", "") row, err := model.GetTaskPluginVersion("kling", "")
require.NoError(t, err) require.NoError(t, err)
assert.False(t, row.Enabled) assert.False(t, row.Enabled)
item := listTaskPluginItem(t, "kling") item := listTaskPluginItem(t, "kling")
assert.Equal(t, "override_over_factory", item.Source) assert.Equal(t, "override_over_factory", item.Source)
assert.False(t, item.Enabled) assert.False(t, item.Enabled)
assert.Equal(t, "disabled_fallback", item.RuntimeStatus) assert.Equal(t, "disabled", item.RuntimeStatus)
assert.True(t, taskPluginOptionsHasKey(t, "kling")) assert.False(t, taskPluginOptionsHasKey(t, "kling"))
_, ok := jsplugin.DefaultRegistry.Get("kling")
assert.False(t, ok, "factory built-in must not keep serving after the plugin is switched off")
recorder = postTaskPluginStatus(t, "kling", "", `{"enabled":true}`)
assert.Contains(t, recorder.Body.String(), `"success":true`)
assert.Empty(t, setting.GetTaskPluginDisabledFactoryKeys())
row, err = model.GetTaskPluginVersion("kling", "")
require.NoError(t, err)
assert.True(t, row.Enabled)
got, ok := jsplugin.DefaultRegistry.Get("kling") got, ok := jsplugin.DefaultRegistry.Get("kling")
require.True(t, ok) require.True(t, ok)
assert.Equal(t, factoryVersion, got.Meta.Version) assert.Equal(t, loaded.Meta.Version, got.Meta.Version)
assert.Equal(t, "registered", listTaskPluginItem(t, "kling").RuntimeStatus)
// Regression: with every kling layer off, a plugin whose model differs from
// a built-in kling model only by case must upload without a routing conflict.
recorder = postTaskPluginStatus(t, "kling", "", `{"enabled":false}`)
require.Contains(t, recorder.Body.String(), `"success":true`)
cleanupTaskPluginControllerRuntime(t, "kling-shadow")
const shadowSource = `
export const meta = {apiVersion: 1, key: "kling-shadow", name: "Shadow", version: "1.0.0", author: {name: "Test"}, models: ["KLING-V1"], fetchMode: "per_task"};
export function buildSubmitRequest() { return {}; }
export function parseSubmitResponse() { return {}; }
export function buildQueryRequest() { return {}; }
export function parseTaskResult() { return {}; }
`
body, err := common.Marshal(map[string]any{"source": shadowSource})
require.NoError(t, err)
uploadRecorder := httptest.NewRecorder()
uploadContext, _ := gin.CreateTestContext(uploadRecorder)
uploadContext.Request = httptest.NewRequest(http.MethodPost, "/api/plugin/task", strings.NewReader(string(body)))
uploadContext.Request.Header.Set("Content-Type", "application/json")
UploadTaskPlugin(uploadContext)
assert.Contains(t, uploadRecorder.Body.String(), `"success":true`)
_, ok = jsplugin.DefaultRegistry.Get("kling-shadow")
assert.True(t, ok)
} }
func TestListTaskPluginsIncludesFactoryWithoutDatabaseRows(t *testing.T) { func TestListTaskPluginsIncludesFactoryWithoutDatabaseRows(t *testing.T) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment