From c0cff23a3af8e68809647bd39ec204bedbea28af Mon Sep 17 00:00:00 2001
From: CaIon
Date: Mon, 21 Sep 2026 13:50:59 +0800
Subject: [PATCH] fix(plugins): store task plugin source as longtext on MySQL
and raise the upload cap to 8 MiB
A bare TEXT column holds 64 KiB on MySQL, and the shipped alibaba plugin
is already 73 KB, so uploading an override of it failed with "Data too
long for column 'source'" or was silently truncated. The 1 MiB upload
cap also blocks the larger plugins planned next.
Add model.LongText, a string type that picks longtext on MySQL and text
on PostgreSQL and SQLite through GormDBDataType, and use it for source
and icon. Neither type carries a length, so AutoMigrate leaves an
up-to-date column alone and widens the shipped MySQL text column once
without data loss. Raise the source cap to 8 MiB on the backend and in
the frontend picker, importers and locale strings.
Verified fresh and rc.39-upgrade startups twice each on SQLite, MySQL
5.7.44 and 8.4.11, PostgreSQL 9.6.24 and 17.11; the second start issues
no DDL and a 73 KB source round-trips on every engine.
---
controller/task_plugin.go | 24 ++---
controller/task_plugin_test.go | 92 +++++++++++++++----
model/task_plugin.go | 46 +++++++---
.../task-plugins/__tests__/plugin-url.test.ts | 3 +-
.../__tests__/upload-dialog.test.tsx | 4 +-
.../components/marketplace-install-dialog.tsx | 2 +-
.../components/plugin-source-picker.tsx | 2 +-
.../components/plugin-url-import-field.tsx | 2 +-
.../task-plugins/components/upload-dialog.tsx | 2 +-
.../features/task-plugins/lib/plugin-url.ts | 2 +-
web/src/i18n/locales/en.json | 4 +-
web/src/i18n/locales/fr.json | 4 +-
web/src/i18n/locales/ja.json | 4 +-
web/src/i18n/locales/ru.json | 4 +-
web/src/i18n/locales/vi.json | 4 +-
web/src/i18n/locales/zh-TW.json | 4 +-
web/src/i18n/locales/zh.json | 4 +-
17 files changed, 141 insertions(+), 66 deletions(-)
diff --git a/controller/task_plugin.go b/controller/task_plugin.go
index 0b27c1183..45db4813d 100644
--- a/controller/task_plugin.go
+++ b/controller/task_plugin.go
@@ -26,7 +26,7 @@ import (
"gorm.io/gorm"
)
-const maxTaskPluginSourceBytes = 1024 * 1024
+const maxTaskPluginSourceBytes = 8 << 20
func taskPluginCompileError(c *gin.Context, err error) {
var unknownField *jsplugin.UnknownMetaFieldError
@@ -55,7 +55,7 @@ func UploadTaskPlugin(c *gin.Context) {
return
}
if len(request.Source) > maxTaskPluginSourceBytes {
- common.ApiErrorMsg(c, "plugin source exceeds 1 MiB")
+ common.ApiErrorMsg(c, "plugin source exceeds 8 MiB")
return
}
if expected := strings.TrimSpace(request.SourceSha256); expected != "" {
@@ -94,8 +94,8 @@ func UploadTaskPlugin(c *gin.Context) {
}
plugin := model.TaskPlugin{
Key: loaded.Meta.Key, APIVersion: loaded.Meta.APIVersion, Version: loaded.Meta.Version,
- Source: request.Source, SourceHash: fmt.Sprintf("%x", sha256.Sum256([]byte(request.Source))),
- Icon: icon, Enabled: enabled, Remark: request.Remark,
+ Source: model.LongText(request.Source), SourceHash: fmt.Sprintf("%x", sha256.Sum256([]byte(request.Source))),
+ Icon: model.LongText(icon), Enabled: enabled, Remark: request.Remark,
}
if err = model.SaveTaskPlugin(&plugin); err != nil {
common.ApiError(c, err)
@@ -105,7 +105,7 @@ func UploadTaskPlugin(c *gin.Context) {
common.ApiError(c, err)
return
}
- common.ApiSuccess(c, taskPluginDetail{Plugin: &plugin, Meta: loaded.Meta, Source: plugin.Source, Layer: "override", HasIcon: plugin.HasIcon()})
+ common.ApiSuccess(c, taskPluginDetail{Plugin: &plugin, Meta: loaded.Meta, Source: string(plugin.Source), Layer: "override", HasIcon: plugin.HasIcon()})
}
func GetTaskPluginVersions(c *gin.Context) {
@@ -195,7 +195,7 @@ func ListTaskPlugins(c *gin.Context) {
item.FactoryMeta = &factoryCopy
}
item.Meta = jsplugin.Meta{Key: row.Key, Version: row.Version, APIVersion: row.APIVersion}
- if compiled, compileErr := jsplugin.NewRegistry().Register(row.Source, jsplugin.Options{Key: row.Key, Version: row.Version}); compileErr == nil {
+ if compiled, compileErr := jsplugin.NewRegistry().Register(string(row.Source), jsplugin.Options{Key: row.Key, Version: row.Version}); compileErr == nil {
item.Meta = compiled.Meta
}
item.Enabled = row.Enabled
@@ -316,7 +316,7 @@ func GetTaskPluginIcon(c *gin.Context) {
icon := ""
plugin, err := model.GetTaskPluginVersion(key, c.Query("version"))
if err == nil {
- icon = plugin.Icon
+ icon = string(plugin.Icon)
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
common.ApiError(c, err)
return
@@ -344,12 +344,12 @@ func GetTaskPlugin(c *gin.Context) {
version := c.Query("version")
plugin, err := model.GetTaskPluginVersion(key, version)
if err == nil {
- loaded, compileErr := jsplugin.NewRegistry().Register(plugin.Source, jsplugin.Options{Key: plugin.Key, Version: plugin.Version})
+ loaded, compileErr := jsplugin.NewRegistry().Register(string(plugin.Source), jsplugin.Options{Key: plugin.Key, Version: plugin.Version})
if compileErr != nil {
taskPluginCompileError(c, compileErr)
return
}
- common.ApiSuccess(c, taskPluginDetail{Plugin: plugin, Meta: loaded.Meta, Source: plugin.Source, Layer: "override", HasIcon: plugin.HasIcon()})
+ common.ApiSuccess(c, taskPluginDetail{Plugin: plugin, Meta: loaded.Meta, Source: string(plugin.Source), Layer: "override", HasIcon: plugin.HasIcon()})
return
}
if !errors.Is(err, gorm.ErrRecordNotFound) || version != "" {
@@ -385,7 +385,7 @@ func DryRunTaskPlugin(c *gin.Context) {
detailSource := ""
plugin, err := model.GetTaskPluginVersion(c.Param("key"), "")
if err == nil {
- detailSource = plugin.Source
+ detailSource = string(plugin.Source)
} else if errors.Is(err, gorm.ErrRecordNotFound) {
detailSource, err = plugins.Source(c.Param("key"))
}
@@ -483,7 +483,7 @@ func ActivateTaskPlugin(c *gin.Context) {
common.ApiErrorMsg(c, "plugin version not found")
return
}
- if _, err = jsplugin.NewRegistry().Register(target.Source, jsplugin.Options{Key: target.Key, Version: target.Version}); err != nil {
+ if _, err = jsplugin.NewRegistry().Register(string(target.Source), jsplugin.Options{Key: target.Key, Version: target.Version}); err != nil {
taskPluginCompileError(c, err)
return
}
@@ -770,7 +770,7 @@ func syncTaskPluginsOnceContext(ctx context.Context) error {
plugin.Key,
plugin.Version,
)
- compiled, compileErr := jsplugin.CompilePlugin(plugin.Source, jsplugin.Options{Key: plugin.Key, Version: plugin.Version})
+ compiled, compileErr := jsplugin.CompilePlugin(string(plugin.Source), jsplugin.Options{Key: plugin.Key, Version: plugin.Version})
if compileErr != nil {
retainedIncumbent := false
if current := currentOverrides[plugin.Key]; current != nil {
diff --git a/controller/task_plugin_test.go b/controller/task_plugin_test.go
index 33edfd481..380dc6bff 100644
--- a/controller/task_plugin_test.go
+++ b/controller/task_plugin_test.go
@@ -272,7 +272,7 @@ func TestDisableFactoryOverrideRowSuppressesBothLayers(t *testing.T) {
t.Cleanup(func() { jsplugin.DefaultRegistry.Unregister("kling") })
plugin := model.TaskPlugin{
Key: "kling", APIVersion: loaded.Meta.APIVersion, Version: loaded.Meta.Version,
- Source: overrideSource, SourceHash: "test-hash", Enabled: true,
+ Source: model.LongText(overrideSource), SourceHash: "test-hash", Enabled: true,
}
require.NoError(t, model.SaveTaskPlugin(&plugin))
require.NoError(t, syncTaskPluginsOnce())
@@ -326,6 +326,64 @@ export function parseTaskResult() { return {}; }
assert.True(t, ok)
}
+// The shipped alibaba plugin is already larger than a MySQL TEXT column
+// (64 KiB), so storing it through the model path the upload endpoint uses and
+// reading it back unchanged proves the source and icon columns are not
+// truncated. Set TEST_TASK_DB_DIALECT plus TEST_MYSQL_DSN or TEST_POSTGRES_DSN
+// to run the same contract against a real MySQL or PostgreSQL server.
+func TestTaskPluginSourceAboveMySQLTextLimitRoundTrips(t *testing.T) {
+ database, _ := openTaskDialectDatabase(t, &model.TaskPlugin{})
+ originalDB := model.DB
+ model.DB = database
+ t.Cleanup(func() { model.DB = originalDB })
+
+ source, err := plugins.Source("alibaba")
+ require.NoError(t, err)
+ require.Greater(t, len(source), 65535, "fixture must exceed the MySQL TEXT limit")
+ icon := "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(strings.Repeat(``, 2048)))
+ require.Greater(t, len(icon), 65535, "icon fixture must exceed the MySQL TEXT limit")
+
+ plugin := model.TaskPlugin{Key: "alibaba", APIVersion: 1, Version: "roundtrip", Source: model.LongText(source), SourceHash: "hash", Enabled: true}
+ require.NoError(t, model.SaveTaskPlugin(&plugin))
+ // Re-saving the same version attaches the icon through the update path.
+ withIcon := model.TaskPlugin{Key: "alibaba", APIVersion: 1, Version: "roundtrip", Source: model.LongText(source), SourceHash: "hash", Icon: model.LongText(icon), Enabled: true}
+ require.NoError(t, model.SaveTaskPlugin(&withIcon))
+
+ stored, err := model.GetTaskPluginVersion("alibaba", "roundtrip")
+ require.NoError(t, err)
+ assert.Equal(t, source, string(stored.Source))
+ assert.Equal(t, icon, string(stored.Icon))
+}
+
+func TestUploadTaskPluginAcceptsSourcesUpToEightMiB(t *testing.T) {
+ setupTaskPluginControllerTest(t)
+ cleanupTaskPluginControllerRuntime(t, "large-source")
+ upload := func(source string) *httptest.ResponseRecorder {
+ body, err := common.Marshal(map[string]any{"source": source})
+ require.NoError(t, err)
+ recorder := httptest.NewRecorder()
+ context, _ := gin.CreateTestContext(recorder)
+ context.Request = httptest.NewRequest(http.MethodPost, "/api/plugin/task", bytes.NewReader(body))
+ context.Request.Header.Set("Content-Type", "application/json")
+ UploadTaskPlugin(context)
+ return recorder
+ }
+
+ // A trailing comment pads a valid plugin past the former 1 MiB cap.
+ accepted := taskPluginControllerTestSource("large-source", "1.0.0") + "// " + strings.Repeat("x", 2<<20) + "\n"
+ recorder := upload(accepted)
+ require.Contains(t, recorder.Body.String(), `"success":true`)
+ stored, err := model.GetTaskPluginVersion("large-source", "1.0.0")
+ require.NoError(t, err)
+ assert.Equal(t, accepted, string(stored.Source))
+
+ rejected := upload(accepted + strings.Repeat("x", 6<<20))
+ assert.Contains(t, rejected.Body.String(), `"success":false`)
+ assert.Contains(t, rejected.Body.String(), "plugin source exceeds 8 MiB")
+ _, err = model.GetTaskPluginVersion("large-source", "2.0.0")
+ assert.ErrorIs(t, err, gorm.ErrRecordNotFound)
+}
+
func TestListTaskPluginsIncludesFactoryWithoutDatabaseRows(t *testing.T) {
setupTaskPluginControllerTest(t)
recorder := httptest.NewRecorder()
@@ -443,7 +501,7 @@ func TestDeleteActiveOverrideFallsBackToFactoryAndDeletesRecord(t *testing.T) {
t.Cleanup(func() { jsplugin.DefaultRegistry.Unregister("kling") })
plugin := model.TaskPlugin{
Key: "kling", APIVersion: loaded.Meta.APIVersion, Version: loaded.Meta.Version,
- Source: overrideSource, SourceHash: "test-hash", Enabled: true,
+ Source: model.LongText(overrideSource), SourceHash: "test-hash", Enabled: true,
}
require.NoError(t, model.SaveTaskPlugin(&plugin))
recorder := httptest.NewRecorder()
@@ -469,10 +527,10 @@ func TestDeleteActiveTaskPluginPromotesEnabledVersionInRuntime(t *testing.T) {
v1Source := taskPluginControllerTestSource(key, "1.0.0")
v2Source := taskPluginControllerTestSource(key, "2.0.0")
require.NoError(t, model.SaveTaskPlugin(&model.TaskPlugin{
- Key: key, APIVersion: 1, Version: "1.0.0", Source: v1Source, SourceHash: "hash-v1", Enabled: true,
+ Key: key, APIVersion: 1, Version: "1.0.0", Source: model.LongText(v1Source), SourceHash: "hash-v1", Enabled: true,
}))
require.NoError(t, model.SaveTaskPlugin(&model.TaskPlugin{
- Key: key, APIVersion: 1, Version: "2.0.0", Source: v2Source, SourceHash: "hash-v2", Enabled: true,
+ Key: key, APIVersion: 1, Version: "2.0.0", Source: model.LongText(v2Source), SourceHash: "hash-v2", Enabled: true,
}))
_, err := jsplugin.DefaultRegistry.Register(v1Source, jsplugin.Options{Key: key, Version: "1.0.0"})
require.NoError(t, err)
@@ -538,10 +596,10 @@ func TestActivateTaskPluginRefreshesRuntimeSyncState(t *testing.T) {
v1Source := taskPluginControllerTestSource(key, "1.0.0")
v2Source := taskPluginControllerTestSource(key, "2.0.0")
require.NoError(t, model.SaveTaskPlugin(&model.TaskPlugin{
- Key: key, APIVersion: 1, Version: "1.0.0", Source: v1Source, SourceHash: "hash-v1", Enabled: true,
+ Key: key, APIVersion: 1, Version: "1.0.0", Source: model.LongText(v1Source), SourceHash: "hash-v1", Enabled: true,
}))
require.NoError(t, model.SaveTaskPlugin(&model.TaskPlugin{
- Key: key, APIVersion: 1, Version: "2.0.0", Source: v2Source, SourceHash: "hash-v2", Enabled: true,
+ Key: key, APIVersion: 1, Version: "2.0.0", Source: model.LongText(v2Source), SourceHash: "hash-v2", Enabled: true,
}))
_, err := jsplugin.DefaultRegistry.Register(v1Source, jsplugin.Options{Key: key, Version: "1.0.0"})
require.NoError(t, err)
@@ -578,10 +636,10 @@ func TestSyncTaskPluginsPublishesOneGenerationForWholeBatch(t *testing.T) {
firstSource := taskPluginControllerTestSource(firstKey, "1.0.0")
secondSource := taskPluginControllerTestSource(secondKey, "1.0.0")
require.NoError(t, model.SaveTaskPlugin(&model.TaskPlugin{
- Key: firstKey, APIVersion: 1, Version: "1.0.0", Source: firstSource, SourceHash: "first-hash", Enabled: true,
+ Key: firstKey, APIVersion: 1, Version: "1.0.0", Source: model.LongText(firstSource), SourceHash: "first-hash", Enabled: true,
}))
require.NoError(t, model.SaveTaskPlugin(&model.TaskPlugin{
- Key: secondKey, APIVersion: 1, Version: "1.0.0", Source: secondSource, SourceHash: "second-hash", Enabled: true,
+ Key: secondKey, APIVersion: 1, Version: "1.0.0", Source: model.LongText(secondSource), SourceHash: "second-hash", Enabled: true,
}))
before := jsplugin.DefaultRegistry.Generation().Number
@@ -614,7 +672,7 @@ func TestTaskPluginRuntimeExposesDatabaseRevisionAheadOfLocalGeneration(t *testi
v1Source := taskPluginControllerTestSource(key, "1.0.0")
require.NoError(t, model.SaveTaskPlugin(&model.TaskPlugin{
Key: key, APIVersion: 1, Version: "1.0.0",
- Source: v1Source, SourceHash: "runtime-v1", Enabled: true,
+ Source: model.LongText(v1Source), SourceHash: "runtime-v1", Enabled: true,
}))
require.NoError(t, syncTaskPluginsOnce())
localGeneration := jsplugin.DefaultRegistry.Generation().Number
@@ -625,7 +683,7 @@ func TestTaskPluginRuntimeExposesDatabaseRevisionAheadOfLocalGeneration(t *testi
v2Source := taskPluginControllerTestSource(key, "2.0.0")
require.NoError(t, model.SaveTaskPlugin(&model.TaskPlugin{
Key: key, APIVersion: 1, Version: "2.0.0",
- Source: v2Source, SourceHash: "runtime-v2", Enabled: true,
+ Source: model.LongText(v2Source), SourceHash: "runtime-v2", Enabled: true,
}))
require.NoError(t, model.ActivateTaskPlugin(key, "2.0.0"))
@@ -705,7 +763,7 @@ func TestTaskPluginRuntimeSurvivesDatabaseSyncFailure(t *testing.T) {
source := taskPluginControllerTestSource(key, "1.0.0")
require.NoError(t, model.SaveTaskPlugin(&model.TaskPlugin{
Key: key, APIVersion: 1, Version: "1.0.0",
- Source: source, SourceHash: "runtime-database-v1", Enabled: true,
+ Source: model.LongText(source), SourceHash: "runtime-database-v1", Enabled: true,
}))
require.NoError(t, syncTaskPluginsOnce())
generation := jsplugin.DefaultRegistry.Generation().Number
@@ -747,17 +805,17 @@ func TestSyncTaskPluginsCachesRejectedDesiredSourceWithoutLosingIncumbent(t *tes
v2Source := taskPluginControllerChannelSource(pluginKey, "2.0.0", 9002)
ownerSource := taskPluginControllerChannelSource(ownerKey, "1.0.0", 9002)
require.NoError(t, model.SaveTaskPlugin(&model.TaskPlugin{
- Key: pluginKey, APIVersion: 1, Version: "1.0.0", Source: v1Source, SourceHash: "retained-v1", Enabled: true,
+ Key: pluginKey, APIVersion: 1, Version: "1.0.0", Source: model.LongText(v1Source), SourceHash: "retained-v1", Enabled: true,
}))
require.NoError(t, model.SaveTaskPlugin(&model.TaskPlugin{
- Key: ownerKey, APIVersion: 1, Version: "1.0.0", Source: ownerSource, SourceHash: "owner-v1", Enabled: true,
+ Key: ownerKey, APIVersion: 1, Version: "1.0.0", Source: model.LongText(ownerSource), SourceHash: "owner-v1", Enabled: true,
}))
require.NoError(t, syncTaskPluginsOnce())
incumbent, ok := jsplugin.DefaultRegistry.Get(pluginKey)
require.True(t, ok)
require.NoError(t, model.SaveTaskPlugin(&model.TaskPlugin{
- Key: pluginKey, APIVersion: 1, Version: "2.0.0", Source: v2Source, SourceHash: "retained-v2", Enabled: true,
+ Key: pluginKey, APIVersion: 1, Version: "2.0.0", Source: model.LongText(v2Source), SourceHash: "retained-v2", Enabled: true,
}))
require.NoError(t, model.ActivateTaskPlugin(pluginKey, "2.0.0"))
@@ -779,7 +837,7 @@ func TestSyncTaskPluginsCachesRejectedDesiredSourceWithoutLosingIncumbent(t *tes
require.NoError(t, model.SaveTaskPlugin(&model.TaskPlugin{
Key: ownerKey, APIVersion: 1, Version: "2.0.0",
- Source: taskPluginControllerChannelSource(ownerKey, "2.0.0", 9003), SourceHash: "owner-v2", Enabled: true,
+ Source: model.LongText(taskPluginControllerChannelSource(ownerKey, "2.0.0", 9003)), SourceHash: "owner-v2", Enabled: true,
}))
require.NoError(t, model.ActivateTaskPlugin(ownerKey, "2.0.0"))
require.NoError(t, syncTaskPluginsOnce())
@@ -1198,7 +1256,7 @@ func TestTaskPluginDisplayOrderAndMetadata(t *testing.T) {
_, err := jsplugin.DefaultRegistry.Register(source, jsplugin.Options{})
require.NoError(t, err)
cleanupTaskPluginControllerRuntime(t, key)
- require.NoError(t, model.SaveTaskPlugin(&model.TaskPlugin{Key: key, Version: "1.0.0", APIVersion: 1, Source: source, Enabled: true}))
+ require.NoError(t, model.SaveTaskPlugin(&model.TaskPlugin{Key: key, Version: "1.0.0", APIVersion: 1, Source: model.LongText(source), Enabled: true}))
}
for _, endpoint := range []string{"list", "options"} {
t.Run(endpoint, func(t *testing.T) {
@@ -1268,7 +1326,7 @@ func TestUploadTaskPluginStoresSidecarIconAndServesIt(t *testing.T) {
assert.NotContains(t, accepted.Body.String(), "base64,", "icon bytes never travel inside detail JSON")
stored, err := model.GetTaskPluginVersion(key, "1.0.0")
require.NoError(t, err)
- assert.Equal(t, svgIcon, stored.Icon)
+ assert.Equal(t, svgIcon, string(stored.Icon))
item := listTaskPluginItem(t, key)
assert.True(t, item.HasIcon)
diff --git a/model/task_plugin.go b/model/task_plugin.go
index adcfd24c7..9f2bdb943 100644
--- a/model/task_plugin.go
+++ b/model/task_plugin.go
@@ -12,6 +12,7 @@ import (
"github.com/QuantumNous/new-api/constant"
"gorm.io/gorm"
"gorm.io/gorm/clause"
+ "gorm.io/gorm/schema"
)
type TaskPluginChannelRef struct {
@@ -57,25 +58,40 @@ func UnbindTaskPlugin(channelID int, key string) (bool, error) {
return true, DB.Model(&Channel{}).Where("id = ?", channelID).Update("setting", channel.Setting).Error
}
+// LongText is a string column sized for plugin payloads on every supported
+// database: longtext on MySQL, text on PostgreSQL and SQLite. A bare
+// `type:text` tag stops at 64 KiB on MySQL. A `size:` tag above that becomes
+// varchar(N) on PostgreSQL, which rejects sizes past 10485760, and a NOT NULL
+// column declared that way is re-altered by AutoMigrate on every MySQL start
+// because the reported column length never equals N. Neither type chosen here
+// carries a length, so an up-to-date column is left alone, and MySQL widens a
+// shipped text column once, without data loss.
+type LongText string
+
+func (LongText) GormDBDataType(db *gorm.DB, _ *schema.Field) string {
+ if db.Dialector.Name() == string(common.DatabaseTypeMySQL) {
+ return "longtext"
+ }
+ return "text"
+}
+
type TaskPlugin struct {
- Id int64 `json:"id"`
- Key string `json:"key" gorm:"size:128;not null;uniqueIndex:uk_task_plugin_key_version,priority:1"`
- APIVersion int `json:"api_version" gorm:"not null"`
- Version string `json:"version" gorm:"size:64;not null;uniqueIndex:uk_task_plugin_key_version,priority:2"`
- Source string `json:"source" gorm:"type:text;not null"`
- SourceHash string `json:"source_hash" gorm:"size:64;not null"`
+ Id int64 `json:"id"`
+ Key string `json:"key" gorm:"size:128;not null;uniqueIndex:uk_task_plugin_key_version,priority:1"`
+ APIVersion int `json:"api_version" gorm:"not null"`
+ Version string `json:"version" gorm:"size:64;not null;uniqueIndex:uk_task_plugin_key_version,priority:2"`
+ Source LongText `json:"source" gorm:"not null"`
+ SourceHash string `json:"source_hash" gorm:"size:64;not null"`
// Icon is the plugin logo shipped as a sidecar icon.svg / icon.png next to
// plugin.js, stored as a data URI so one column carries both the media
// type and the bytes. It never travels inside list or detail JSON; the UI
- // loads it through GET /api/plugin/task/:key/icon. size matches the
- // 512 KiB icon cap and makes GORM emit mediumtext on MySQL (a bare TEXT
- // column there holds only 64 KiB), varchar(524288) on PostgreSQL, and text
- // on SQLite.
- Icon string `json:"-" gorm:"size:524288"`
- Enabled bool `json:"enabled" gorm:"not null"`
- Active bool `json:"active" gorm:"not null;index"`
- CreatedAt int64 `json:"created_at" gorm:"not null"`
- Remark string `json:"remark" gorm:"type:text"`
+ // loads it through GET /api/plugin/task/:key/icon. LongText keeps the
+ // 512 KiB icon cap storable on MySQL, where a bare TEXT holds only 64 KiB.
+ Icon LongText `json:"-"`
+ Enabled bool `json:"enabled" gorm:"not null"`
+ Active bool `json:"active" gorm:"not null;index"`
+ CreatedAt int64 `json:"created_at" gorm:"not null"`
+ Remark string `json:"remark" gorm:"type:text"`
}
// HasIcon reports whether this version ships a logo.
diff --git a/web/src/features/task-plugins/__tests__/plugin-url.test.ts b/web/src/features/task-plugins/__tests__/plugin-url.test.ts
index efa517ffd..3f2e17603 100644
--- a/web/src/features/task-plugins/__tests__/plugin-url.test.ts
+++ b/web/src/features/task-plugins/__tests__/plugin-url.test.ts
@@ -17,6 +17,7 @@ along with this program. If not, see
{t( - 'Single .js file, up to 1 MiB. Its source is shown below before upload.' + 'Single .js file, up to 8 MiB. Its source is shown below before upload.' )}
diff --git a/web/src/features/task-plugins/components/plugin-url-import-field.tsx b/web/src/features/task-plugins/components/plugin-url-import-field.tsx index c8e4299be..c766869ad 100644 --- a/web/src/features/task-plugins/components/plugin-url-import-field.tsx +++ b/web/src/features/task-plugins/components/plugin-url-import-field.tsx @@ -73,7 +73,7 @@ export function PluginUrlImportField(props: PluginUrlImportFieldProps) { return } if (error.reason === 'too_large') { - props.onError(t('Plugin source exceeds the 1 MiB limit.')) + props.onError(t('Plugin source exceeds the 8 MiB limit.')) return } if (error.reason === 'not_found') { diff --git a/web/src/features/task-plugins/components/upload-dialog.tsx b/web/src/features/task-plugins/components/upload-dialog.tsx index 9894e37d6..1207e6788 100644 --- a/web/src/features/task-plugins/components/upload-dialog.tsx +++ b/web/src/features/task-plugins/components/upload-dialog.tsx @@ -108,7 +108,7 @@ export function UploadDialog(props: UploadDialogProps) { const handleFile = async (file: File) => { if (file.size > MAX_PLUGIN_SOURCE_BYTES) { - setImportError(t('Plugin source exceeds the 1 MiB limit.')) + setImportError(t('Plugin source exceeds the 8 MiB limit.')) return } setImportError('') diff --git a/web/src/features/task-plugins/lib/plugin-url.ts b/web/src/features/task-plugins/lib/plugin-url.ts index 9d932aff1..6b3e3cf22 100644 --- a/web/src/features/task-plugins/lib/plugin-url.ts +++ b/web/src/features/task-plugins/lib/plugin-url.ts @@ -21,7 +21,7 @@ For commercial licensing, please contact support@quantumnous.com * controller/task_plugin.go). Enforced client-side too so an oversized fetch * fails with a readable message instead of a server rejection. */ -export const MAX_PLUGIN_SOURCE_BYTES = 1024 * 1024 +export const MAX_PLUGIN_SOURCE_BYTES = 8 * 1024 * 1024 export function pluginSourceByteLength(source: string): number { return new TextEncoder().encode(source).length diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 12faa663d..8ce9da227 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -4371,7 +4371,7 @@ "Plugin sandbox": "Plugin sandbox", "Plugin saved, activation required": "Plugin saved, activation required", "Plugin source": "Plugin source", - "Plugin source exceeds the 1 MiB limit.": "Plugin source exceeds the 1 MiB limit.", + "Plugin source exceeds the 8 MiB limit.": "Plugin source exceeds the 8 MiB limit.", "Plugin uploaded successfully": "Plugin uploaded successfully", "Plugin version activated": "Plugin version activated", "Plugin version deleted": "Plugin version deleted", @@ -5429,7 +5429,7 @@ "Simulation context is missing.": "Simulation context is missing.", "Simulation time": "Simulation time", "Since previous sample": "Since previous sample", - "Single .js file, up to 1 MiB. Its source is shown below before upload.": "Single .js file, up to 1 MiB. Its source is shown below before upload.", + "Single .js file, up to 8 MiB. Its source is shown below before upload.": "Single .js file, up to 8 MiB. Its source is shown below before upload.", "Single Key": "Single Key", "Site & Branding": "Site & Branding", "Site currency ({{currency}})": "Site currency ({{currency}})", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 1dfdf8ead..a239f2062 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -4371,7 +4371,7 @@ "Plugin sandbox": "Bac à sable du plugin", "Plugin saved, activation required": "Plugin enregistré, activation requise", "Plugin source": "Source du plugin", - "Plugin source exceeds the 1 MiB limit.": "Le code du plugin dépasse la limite de 1 Mio.", + "Plugin source exceeds the 8 MiB limit.": "Le code du plugin dépasse la limite de 8 Mio.", "Plugin uploaded successfully": "Plugin importé avec succès", "Plugin version activated": "Version du plugin activée", "Plugin version deleted": "Version du plugin supprimée", @@ -5429,7 +5429,7 @@ "Simulation context is missing.": "Le contexte nécessaire à la simulation est manquant.", "Simulation time": "Heure de simulation", "Since previous sample": "Depuis le relevé précédent", - "Single .js file, up to 1 MiB. Its source is shown below before upload.": "Un seul fichier .js, jusqu’à 1 Mio. Sa source est affichée ci-dessous avant l’envoi.", + "Single .js file, up to 8 MiB. Its source is shown below before upload.": "Un seul fichier .js, jusqu’à 8 Mio. Sa source est affichée ci-dessous avant l’envoi.", "Single Key": "Clé unique", "Site & Branding": "Site et marque", "Site currency ({{currency}})": "Devise du site ({{currency}})", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index b6c85ea38..e3e092617 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -4371,7 +4371,7 @@ "Plugin sandbox": "プラグインサンドボックス", "Plugin saved, activation required": "プラグインを保存しました。有効化が必要です", "Plugin source": "プラグインソース", - "Plugin source exceeds the 1 MiB limit.": "プラグインのソースが 1 MiB の上限を超えています。", + "Plugin source exceeds the 8 MiB limit.": "プラグインのソースが 8 MiB の上限を超えています。", "Plugin uploaded successfully": "プラグインをアップロードしました", "Plugin version activated": "プラグインバージョンを有効化しました", "Plugin version deleted": "プラグインバージョンを削除しました", @@ -5429,7 +5429,7 @@ "Simulation context is missing.": "シミュレーションに必要なコンテキストがありません。", "Simulation time": "シミュレーション時刻", "Since previous sample": "前回のサンプル以降", - "Single .js file, up to 1 MiB. Its source is shown below before upload.": "単一の .js ファイル、最大 1 MiB。アップロード前に下部でソースを確認できます。", + "Single .js file, up to 8 MiB. Its source is shown below before upload.": "単一の .js ファイル、最大 8 MiB。アップロード前に下部でソースを確認できます。", "Single Key": "単一キー", "Site & Branding": "サイトとブランド", "Site currency ({{currency}})": "サイト通貨({{currency}})", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index e9614fb14..af18b036b 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -4371,7 +4371,7 @@ "Plugin sandbox": "Песочница плагина", "Plugin saved, activation required": "Плагин сохранён, требуется активация", "Plugin source": "Исходный код плагина", - "Plugin source exceeds the 1 MiB limit.": "Код плагина превышает лимит 1 МиБ.", + "Plugin source exceeds the 8 MiB limit.": "Код плагина превышает лимит 8 МиБ.", "Plugin uploaded successfully": "Плагин успешно загружен", "Plugin version activated": "Версия плагина активирована", "Plugin version deleted": "Версия плагина удалена", @@ -5429,7 +5429,7 @@ "Simulation context is missing.": "Отсутствует контекст для моделирования.", "Simulation time": "Время моделирования", "Since previous sample": "С предыдущей выборки", - "Single .js file, up to 1 MiB. Its source is shown below before upload.": "Один файл .js размером до 1 МиБ. Его исходный код показан ниже перед загрузкой.", + "Single .js file, up to 8 MiB. Its source is shown below before upload.": "Один файл .js размером до 8 МиБ. Его исходный код показан ниже перед загрузкой.", "Single Key": "Одиночный ключ", "Site & Branding": "Сайт и брендинг", "Site currency ({{currency}})": "Валюта сайта ({{currency}})", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index 5387378fb..c7d3e7199 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -4371,7 +4371,7 @@ "Plugin sandbox": "Môi trường thử nghiệm plugin", "Plugin saved, activation required": "Đã lưu plugin, cần kích hoạt", "Plugin source": "Mã nguồn plugin", - "Plugin source exceeds the 1 MiB limit.": "Mã nguồn plugin vượt giới hạn 1 MiB.", + "Plugin source exceeds the 8 MiB limit.": "Mã nguồn plugin vượt giới hạn 8 MiB.", "Plugin uploaded successfully": "Đã tải plugin lên", "Plugin version activated": "Đã kích hoạt phiên bản plugin", "Plugin version deleted": "Đã xóa phiên bản plugin", @@ -5429,7 +5429,7 @@ "Simulation context is missing.": "Thiếu ngữ cảnh cần thiết để mô phỏng.", "Simulation time": "Thời gian mô phỏng", "Since previous sample": "Kể từ mẫu trước", - "Single .js file, up to 1 MiB. Its source is shown below before upload.": "Một tệp .js duy nhất, tối đa 1 MiB. Mã nguồn được hiển thị bên dưới trước khi tải lên.", + "Single .js file, up to 8 MiB. Its source is shown below before upload.": "Một tệp .js duy nhất, tối đa 8 MiB. Mã nguồn được hiển thị bên dưới trước khi tải lên.", "Single Key": "Khóa đơn", "Site & Branding": "Trang web & thương hiệu", "Site currency ({{currency}})": "Tiền tệ trang web ({{currency}})", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index 784a7046f..3eca7eea6 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -4371,7 +4371,7 @@ "Plugin sandbox": "外掛沙盒", "Plugin saved, activation required": "外掛已儲存,待啟用", "Plugin source": "外掛原始碼", - "Plugin source exceeds the 1 MiB limit.": "外掛原始碼超過 1 MiB 上限。", + "Plugin source exceeds the 8 MiB limit.": "外掛原始碼超過 8 MiB 上限。", "Plugin uploaded successfully": "外掛上傳成功", "Plugin version activated": "外掛版本已激活", "Plugin version deleted": "外掛版本已刪除", @@ -5429,7 +5429,7 @@ "Simulation context is missing.": "缺少模擬所需的內容。", "Simulation time": "模擬時間", "Since previous sample": "自上次取樣以來", - "Single .js file, up to 1 MiB. Its source is shown below before upload.": "單一 .js 檔案,最大 1 MiB。上傳前會在下方顯示其原始碼。", + "Single .js file, up to 8 MiB. Its source is shown below before upload.": "單一 .js 檔案,最大 8 MiB。上傳前會在下方顯示其原始碼。", "Single Key": "單金鑰", "Site & Branding": "站點與品牌", "Site currency ({{currency}})": "站點貨幣({{currency}})", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 2b4a5e85a..23de155c0 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -4371,7 +4371,7 @@ "Plugin sandbox": "插件沙盒", "Plugin saved, activation required": "插件已保存,待激活", "Plugin source": "插件源码", - "Plugin source exceeds the 1 MiB limit.": "插件源码超过 1 MiB 上限。", + "Plugin source exceeds the 8 MiB limit.": "插件源码超过 8 MiB 上限。", "Plugin uploaded successfully": "插件上传成功", "Plugin version activated": "插件版本已激活", "Plugin version deleted": "插件版本已删除", @@ -5429,7 +5429,7 @@ "Simulation context is missing.": "缺少模拟所需的上下文。", "Simulation time": "模拟时间", "Since previous sample": "自上次采样以来", - "Single .js file, up to 1 MiB. Its source is shown below before upload.": "单个 .js 文件,最大 1 MiB。上传前会在下方显示其源码。", + "Single .js file, up to 8 MiB. Its source is shown below before upload.": "单个 .js 文件,最大 8 MiB。上传前会在下方显示其源码。", "Single Key": "单密钥", "Site & Branding": "站点与品牌", "Site currency ({{currency}})": "站点货币({{currency}})",