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 . For commercial licensing, please contact support@quantumnous.com */ import assert from 'node:assert/strict' + import { describe, test } from 'vitest' import { @@ -170,7 +171,7 @@ describe('browser plugin source fetch', () => { ) }) - test('rejects a declared content-length above the 1 MiB backend limit before reading the body', async () => { + test('rejects a declared content-length above the 8 MiB backend limit before reading the body', async () => { let bodyRead = false await assert.rejects( fetchPluginSourceText('https://example.com/huge.js', async () => { diff --git a/web/src/features/task-plugins/__tests__/upload-dialog.test.tsx b/web/src/features/task-plugins/__tests__/upload-dialog.test.tsx index 47a71df32..df0a2b854 100644 --- a/web/src/features/task-plugins/__tests__/upload-dialog.test.tsx +++ b/web/src/features/task-plugins/__tests__/upload-dialog.test.tsx @@ -168,7 +168,7 @@ describe('UploadDialog file selection', () => { ).toBeInTheDocument() }) - test('rejects a file over the 1 MiB limit without touching the source', async () => { + test('rejects a file over the 8 MiB limit without touching the source', async () => { const user = userEvent.setup() renderDialog() @@ -179,7 +179,7 @@ describe('UploadDialog file selection', () => { await user.upload(fileInput(), oversized) expect( - await screen.findByText('Plugin source exceeds the 1 MiB limit.') + await screen.findByText('Plugin source exceeds the 8 MiB limit.') ).toBeInTheDocument() expect(screen.queryByText('huge.js')).toBeNull() expect(footerButton('Upload')).toBeDisabled() diff --git a/web/src/features/task-plugins/components/marketplace-install-dialog.tsx b/web/src/features/task-plugins/components/marketplace-install-dialog.tsx index b13a74796..63bc2203f 100644 --- a/web/src/features/task-plugins/components/marketplace-install-dialog.tsx +++ b/web/src/features/task-plugins/components/marketplace-install-dialog.tsx @@ -188,7 +188,7 @@ function MarketplaceInstallContent( if (fetchError instanceof PluginSourceFetchError) { fetchErrorMessage = fetchError.reason === 'too_large' - ? t('Plugin source exceeds the 1 MiB limit.') + ? t('Plugin source exceeds the 8 MiB limit.') : t( 'Could not fetch the plugin source from this browser. The host may block cross-origin requests or be unreachable.' ) diff --git a/web/src/features/task-plugins/components/plugin-source-picker.tsx b/web/src/features/task-plugins/components/plugin-source-picker.tsx index 062f3e574..554094a49 100644 --- a/web/src/features/task-plugins/components/plugin-source-picker.tsx +++ b/web/src/features/task-plugins/components/plugin-source-picker.tsx @@ -77,7 +77,7 @@ export function PluginSourcePicker(props: PluginSourcePickerProps) {

{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}})",