From e537dc380bb253d4f902ac033ab39dd2ca462b5e Mon Sep 17 00:00:00 2001 From: CaIon Date: Mon, 21 Sep 2026 13:36:30 +0800 Subject: [PATCH] fix(web): expose plugin activation after upload --- .../__tests__/upload-dialog.test.tsx | 165 +++++++++++++++++- .../task-plugins/components/upload-dialog.tsx | 143 ++++++++++++--- web/src/i18n/locales/en.json | 6 + web/src/i18n/locales/fr.json | 6 + web/src/i18n/locales/ja.json | 6 + web/src/i18n/locales/ru.json | 6 + web/src/i18n/locales/vi.json | 6 + web/src/i18n/locales/zh-TW.json | 6 + web/src/i18n/locales/zh.json | 6 + 9 files changed, 314 insertions(+), 36 deletions(-) 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 355c1b0e9..47a71df32 100644 --- a/web/src/features/task-plugins/__tests__/upload-dialog.test.tsx +++ b/web/src/features/task-plugins/__tests__/upload-dialog.test.tsx @@ -23,14 +23,18 @@ import { afterEach, describe, expect, test, vi } from 'vitest' import { UploadDialog } from '../components/upload-dialog' import { MAX_PLUGIN_SOURCE_BYTES } from '../lib/plugin-url' +import type { TaskPluginDetail } from '../types' -const uploadTaskPlugin = vi.hoisted(() => vi.fn()) +const { uploadTaskPlugin, activateTaskPlugin } = vi.hoisted(() => ({ + uploadTaskPlugin: vi.fn(), + activateTaskPlugin: vi.fn(), +})) -vi.mock('../api', () => ({ uploadTaskPlugin })) +vi.mock('../api', () => ({ uploadTaskPlugin, activateTaskPlugin })) const queryClients: QueryClient[] = [] -function renderDialog(open = true) { +function renderDialog(open = true, initialKey?: string) { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Number.POSITIVE_INFINITY }, @@ -41,7 +45,11 @@ function renderDialog(open = true) { const onOpenChange = vi.fn() const view = render( - + ) return { onOpenChange, queryClient, view } @@ -61,6 +69,43 @@ function footerButton(name: string | RegExp) { return within(footer as HTMLElement).getByRole('button', { name }) } +function savedPlugin(active: boolean): TaskPluginDetail { + return { + meta: { + key: 'demo', + name: 'Demo', + version: '2.0.0', + apiVersion: 1, + author: { name: 'Demo' }, + models: [], + fetchMode: 'per_task', + }, + source: 'const a = 1', + layer: 'override', + plugin: { + id: 2, + key: 'demo', + api_version: 1, + version: '2.0.0', + source: 'const a = 1', + source_hash: 'hash', + enabled: true, + active, + created_at: 1, + remark: '', + }, + } +} + +async function uploadSource(user: ReturnType) { + await user.upload( + fileInput(), + new File(['const a = 1'], 'plugin.js', { type: 'text/javascript' }) + ) + await waitFor(() => expect(footerButton('Upload')).toBeEnabled()) + await user.click(footerButton('Upload')) +} + afterEach(() => { for (const queryClient of queryClients) queryClient.clear() queryClients.length = 0 @@ -187,6 +232,111 @@ describe('UploadDialog URL import', () => { }) describe('UploadDialog upload lifecycle', () => { + test.each([undefined, 'demo'])( + 'offers activation after saving an inactive version from %s', + async (initialKey) => { + const user = userEvent.setup() + uploadTaskPlugin.mockResolvedValue(savedPlugin(false)) + let resolveActivation!: () => void + activateTaskPlugin.mockImplementation( + () => + new Promise((resolve) => { + resolveActivation = resolve + }) + ) + const { queryClient, onOpenChange } = renderDialog(true, initialKey) + queryClient.setQueryData(['task-plugin', 'demo'], { cached: true }) + queryClient.setQueryData(['task-plugin-versions', 'demo'], []) + queryClient.setQueryData(['task-plugins'], []) + + await uploadSource(user) + + expect( + await screen.findByText('Plugin saved, activation required') + ).toBeVisible() + expect( + screen.getByText( + 'This version is saved but not active. Activate it to replace the current version, or activate it later from the Versions tab.' + ) + ).toBeVisible() + expect( + screen.queryByRole('textbox', { name: 'Plugin source' }) + ).not.toBeInTheDocument() + expect(activateTaskPlugin).not.toHaveBeenCalled() + expect(footerButton('Activate now')).toHaveFocus() + // Reset the cached queries so activation must invalidate them again. + queryClient.setQueryData(['task-plugin', 'demo'], { cached: true }) + queryClient.setQueryData(['task-plugin-versions', 'demo'], []) + queryClient.setQueryData(['task-plugins'], []) + await user.keyboard('{Enter}') + + expect(activateTaskPlugin).toHaveBeenCalledWith('demo', '2.0.0') + expect(footerButton('Activating...')).toBeDisabled() + expect(footerButton('Later')).toBeDisabled() + await user.keyboard('{Escape}') + expect(onOpenChange).not.toHaveBeenCalled() + resolveActivation() + + expect(await screen.findByText('Plugin version activated')).toBeVisible() + expect( + screen.queryByRole('button', { name: 'Activate now' }) + ).not.toBeInTheDocument() + expect(footerButton('Close')).toHaveFocus() + for (const queryKey of [ + ['task-plugins'], + ['task-plugin', 'demo'], + ['task-plugin-versions', 'demo'], + ]) { + expect(queryClient.getQueryState(queryKey)?.isInvalidated).toBe(true) + } + } + ) + + test('keeps the saved version available to retry when activation fails', async () => { + const user = userEvent.setup() + uploadTaskPlugin.mockResolvedValue(savedPlugin(false)) + activateTaskPlugin + .mockRejectedValueOnce(new Error('activation failed')) + .mockResolvedValueOnce(undefined) + renderDialog() + await uploadSource(user) + await user.click( + await screen.findByRole('button', { name: 'Activate now' }) + ) + + expect(await screen.findByText('activation failed')).toBeVisible() + expect(screen.getByText('Plugin saved, activation required')).toBeVisible() + expect(footerButton('Activate now')).toBeEnabled() + await user.click(footerButton('Activate now')) + expect(await screen.findByText('Plugin version activated')).toBeVisible() + expect(uploadTaskPlugin).toHaveBeenCalledTimes(1) + }) + + test('allows postponing activation without changing the active version', async () => { + const user = userEvent.setup() + uploadTaskPlugin.mockResolvedValue(savedPlugin(false)) + const { onOpenChange } = renderDialog() + await uploadSource(user) + await user.click(await screen.findByRole('button', { name: 'Later' })) + expect(onOpenChange).toHaveBeenCalledWith(false) + expect(activateTaskPlugin).not.toHaveBeenCalled() + }) + + test('shows an already active upload as activated without another activation request', async () => { + const user = userEvent.setup() + uploadTaskPlugin.mockResolvedValue(savedPlugin(true)) + renderDialog() + await uploadSource(user) + expect(await screen.findByText('Plugin version activated')).toBeVisible() + expect( + screen.queryByRole('button', { name: 'Activate now' }) + ).not.toBeInTheDocument() + expect( + screen.queryByRole('button', { name: /^Upload$/ }) + ).not.toBeInTheDocument() + expect(activateTaskPlugin).not.toHaveBeenCalled() + }) + test('disables Upload while the source is empty and shows a pending label', async () => { const user = userEvent.setup() let resolveUpload: (value: unknown) => void = () => undefined @@ -210,12 +360,9 @@ describe('UploadDialog upload lifecycle', () => { await user.click(uploadButton) await waitFor(() => expect(footerButton(/Uploading/)).toBeDisabled()) - resolveUpload({ - source: 'const a = 1', - meta: { key: 'demo', name: 'Demo', version: '1.0.0', apiVersion: 1 }, - }) + resolveUpload(savedPlugin(true)) expect( - await screen.findByText('Parsed plugin metadata') + await screen.findByText('Plugin version activated') ).toBeInTheDocument() }) diff --git a/web/src/features/task-plugins/components/upload-dialog.tsx b/web/src/features/task-plugins/components/upload-dialog.tsx index 8bb2d7499..9894e37d6 100644 --- a/web/src/features/task-plugins/components/upload-dialog.tsx +++ b/web/src/features/task-plugins/components/upload-dialog.tsx @@ -17,8 +17,8 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useMutation, useQueryClient } from '@tanstack/react-query' -import { AlertTriangle, CircleCheck, Upload } from 'lucide-react' -import { useState } from 'react' +import { AlertTriangle, CircleCheck, Power, Upload } from 'lucide-react' +import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' @@ -36,7 +36,7 @@ import { import { Input } from '@/components/ui/input' import { Spinner } from '@/components/ui/spinner' -import { uploadTaskPlugin } from '../api' +import { activateTaskPlugin, uploadTaskPlugin } from '../api' import { encodePluginIconFile, PluginIconFileError, @@ -68,22 +68,43 @@ export function UploadDialog(props: UploadDialogProps) { const [result, setResult] = useState(null) const [importUrl, setImportUrl] = useState('') const [importError, setImportError] = useState('') + const primaryActionRef = useRef(null) const mutation = useMutation({ mutationFn: () => uploadTaskPlugin(source, remark, icon), onSuccess: (data) => { setResult(data) queryClient.invalidateQueries({ queryKey: ['task-plugins'] }) - if (props.initialKey) { - queryClient.invalidateQueries({ - queryKey: ['task-plugin', props.initialKey], - }) - queryClient.invalidateQueries({ - queryKey: ['task-plugin-versions', props.initialKey], - }) - } - toast.success(t('Plugin uploaded successfully')) + queryClient.invalidateQueries({ + queryKey: ['task-plugin', data.meta.key], + }) + queryClient.invalidateQueries({ + queryKey: ['task-plugin-versions', data.meta.key], + }) + toast.success( + data.plugin?.active === false + ? t('Plugin saved, activation required') + : t('Plugin uploaded successfully') + ) }, }) + const activateMutation = useMutation({ + mutationFn: (detail: TaskPluginDetail) => + activateTaskPlugin(detail.meta.key, detail.meta.version), + onSuccess: (_, detail) => { + queryClient.invalidateQueries({ queryKey: ['task-plugins'] }) + queryClient.invalidateQueries({ + queryKey: ['task-plugin', detail.meta.key], + }) + queryClient.invalidateQueries({ + queryKey: ['task-plugin-versions', detail.meta.key], + }) + toast.success(t('Plugin version activated')) + }, + }) + + useEffect(() => { + if (result) primaryActionRef.current?.focus() + }, [result, activateMutation.isSuccess]) const handleFile = async (file: File) => { if (file.size > MAX_PLUGIN_SOURCE_BYTES) { @@ -118,6 +139,7 @@ export function UploadDialog(props: UploadDialogProps) { } const close = (open: boolean) => { + if (!open && (mutation.isPending || activateMutation.isPending)) return props.onOpenChange(open) if (!open) { setSource('') @@ -130,13 +152,89 @@ export function UploadDialog(props: UploadDialogProps) { setImportUrl('') setImportError('') mutation.reset() + activateMutation.reset() } } + if (result) { + const needsActivation = + result.plugin?.active === false && !activateMutation.isSuccess + + return ( + + + {needsActivation && ( + + )} + + } + > + + {needsActivation ? ( + + {activateMutation.error && ( + + + )} + + ) + } + return ( - ) } diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index fb8095631..12faa663d 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -217,6 +217,8 @@ "Actions": "Actions", "Actions for {{name}}": "Actions for {{name}}", "Activate / Roll back": "Activate / Roll back", + "Activate now": "Activate now", + "Activating...": "Activating...", "active": "active", "Active": "Active", "Active apps": "Active apps", @@ -3141,6 +3143,7 @@ "Latency check": "Latency check", "Latency short": "Lat.", "Latency trend (last 24h)": "Latency trend (last 24h)", + "Later": "Later", "Later requests follow the new channel.": "Later requests follow the new channel.", "Latest platform updates and notices": "Latest platform updates and notices", "Latest version": "Latest version", @@ -4351,6 +4354,7 @@ "Please wait before requesting another verification code.": "Please wait before requesting another verification code.", "Please wait for the current generation to complete": "Please wait for the current generation to complete", "Plugin": "Plugin", + "Plugin activation failed": "Plugin activation failed", "Plugin author": "Plugin author", "Plugin default": "Plugin default", "Plugin details": "Plugin details", @@ -4365,6 +4369,7 @@ "Plugin key": "Plugin key", "Plugin metadata": "Plugin metadata", "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 uploaded successfully": "Plugin uploaded successfully", @@ -5947,6 +5952,7 @@ "This verification method is not allowed for this action.": "This verification method is not allowed for this action.", "This verification method requires its dedicated verification flow.": "This verification method requires its dedicated verification flow.", "This version is ignored": "This version is ignored", + "This version is saved but not active. Activate it to replace the current version, or activate it later from the Versions tab.": "This version is saved but not active. Activate it to replace the current version, or activate it later from the Versions tab.", "This week": "This week", "This will append 2 template rules (Codex CLI and Claude CLI) to the existing rule list.": "This will append 2 template rules (Codex CLI and Claude CLI) to the existing rule list.", "This will clear custom pricing ratios and revert to upstream defaults.": "This will clear custom pricing ratios and revert to upstream defaults.", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index f55e38e9a..1dfdf8ead 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -217,6 +217,8 @@ "Actions": "Actions", "Actions for {{name}}": "Actions pour {{name}}", "Activate / Roll back": "Activer / Restaurer", + "Activate now": "Activer maintenant", + "Activating...": "Activation...", "active": "actif", "Active": "Actif", "Active apps": "Applications actives", @@ -3141,6 +3143,7 @@ "Latency check": "Contrôle de latence", "Latency short": "Lat.", "Latency trend (last 24h)": "Tendance de latence (24 dernières heures)", + "Later": "Plus tard", "Later requests follow the new channel.": "Les requêtes suivantes suivent le nouveau canal.", "Latest platform updates and notices": "Dernières mises à jour et annonces de la plateforme", "Latest version": "Dernière version", @@ -4351,6 +4354,7 @@ "Please wait before requesting another verification code.": "Veuillez patienter avant de demander un nouveau code.", "Please wait for the current generation to complete": "Veuillez attendre la fin de la génération en cours", "Plugin": "Plugin", + "Plugin activation failed": "Échec de l’activation du plugin", "Plugin author": "Auteur du plugin", "Plugin default": "Valeur par défaut du plugin", "Plugin details": "Détails du plugin", @@ -4365,6 +4369,7 @@ "Plugin key": "Clé du plugin", "Plugin metadata": "Métadonnées du plugin", "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 uploaded successfully": "Plugin importé avec succès", @@ -5947,6 +5952,7 @@ "This verification method is not allowed for this action.": "Cette méthode de vérification n’est pas autorisée pour cette action.", "This verification method requires its dedicated verification flow.": "Cette méthode nécessite son propre parcours de vérification.", "This version is ignored": "Cette version est ignorée", + "This version is saved but not active. Activate it to replace the current version, or activate it later from the Versions tab.": "Cette version est enregistrée mais inactive. Activez-la pour remplacer la version actuelle, ou activez-la plus tard dans l’onglet Versions.", "This week": "Cette semaine", "This will append 2 template rules (Codex CLI and Claude CLI) to the existing rule list.": "Cela ajoutera 2 règles modèles (Codex CLI et Claude CLI) à la liste de règles existante.", "This will clear custom pricing ratios and revert to upstream defaults.": "Cela effacera les ratios de tarification personnalisés et rétablira les valeurs par défaut du fournisseur.", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index 1010f0275..b6c85ea38 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -217,6 +217,8 @@ "Actions": "操作", "Actions for {{name}}": "{{name}} の操作", "Activate / Roll back": "有効化 / ロールバック", + "Activate now": "今すぐ有効化", + "Activating...": "有効化中...", "active": "有効", "Active": "有効", "Active apps": "アクティブなアプリ", @@ -3141,6 +3143,7 @@ "Latency check": "レイテンシ確認", "Latency short": "遅延", "Latency trend (last 24h)": "レイテンシ推移(直近 24 時間)", + "Later": "後で", "Later requests follow the new channel.": "以降のリクエストは新しいチャネルを使用します。", "Latest platform updates and notices": "最新のプラットフォーム更新と通知", "Latest version": "最新バージョン", @@ -4351,6 +4354,7 @@ "Please wait before requesting another verification code.": "しばらく待ってから確認コードを再送信してください。", "Please wait for the current generation to complete": "現在の生成が完了するまでお待ちください", "Plugin": "プラグイン", + "Plugin activation failed": "プラグインの有効化に失敗しました", "Plugin author": "プラグイン作成者", "Plugin default": "プラグインの既定値", "Plugin details": "プラグインの詳細", @@ -4365,6 +4369,7 @@ "Plugin key": "プラグインキー", "Plugin metadata": "プラグインメタデータ", "Plugin sandbox": "プラグインサンドボックス", + "Plugin saved, activation required": "プラグインを保存しました。有効化が必要です", "Plugin source": "プラグインソース", "Plugin source exceeds the 1 MiB limit.": "プラグインのソースが 1 MiB の上限を超えています。", "Plugin uploaded successfully": "プラグインをアップロードしました", @@ -5947,6 +5952,7 @@ "This verification method is not allowed for this action.": "この操作ではこの確認方法を使用できません。", "This verification method requires its dedicated verification flow.": "この確認方法では専用の確認フローを使用してください。", "This version is ignored": "このバージョンの通知は停止中です", + "This version is saved but not active. Activate it to replace the current version, or activate it later from the Versions tab.": "このバージョンは保存済みですが、まだ有効ではありません。有効化すると現在のバージョンを置き換えます。後で「バージョン」タブから有効化することもできます。", "This week": "今週", "This will append 2 template rules (Codex CLI and Claude CLI) to the existing rule list.": "既存のルールリストに2つのテンプレートルール(Codex CLIとClaude CLI)を追加します。", "This will clear custom pricing ratios and revert to upstream defaults.": "これにより、カスタム料金比率がクリアされ、上位のデフォルトに戻ります。", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index e4a5da00a..e9614fb14 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -217,6 +217,8 @@ "Actions": "Операции", "Actions for {{name}}": "Действия для {{name}}", "Activate / Roll back": "Активировать / Откатить", + "Activate now": "Активировать сейчас", + "Activating...": "Активация...", "active": "активный", "Active": "Активна", "Active apps": "Активные приложения", @@ -3141,6 +3143,7 @@ "Latency check": "Проверка задержки", "Latency short": "Зад.", "Latency trend (last 24h)": "Тренд задержки (за 24 часа)", + "Later": "Позже", "Later requests follow the new channel.": "Последующие запросы используют новый канал.", "Latest platform updates and notices": "Последние обновления и уведомления платформы", "Latest version": "Последняя версия", @@ -4351,6 +4354,7 @@ "Please wait before requesting another verification code.": "Подождите перед повторным запросом кода подтверждения.", "Please wait for the current generation to complete": "Дождитесь завершения текущей генерации", "Plugin": "Плагин", + "Plugin activation failed": "Не удалось активировать плагин", "Plugin author": "Автор плагина", "Plugin default": "Значение плагина по умолчанию", "Plugin details": "Сведения о плагине", @@ -4365,6 +4369,7 @@ "Plugin key": "Ключ плагина", "Plugin metadata": "Метаданные плагина", "Plugin sandbox": "Песочница плагина", + "Plugin saved, activation required": "Плагин сохранён, требуется активация", "Plugin source": "Исходный код плагина", "Plugin source exceeds the 1 MiB limit.": "Код плагина превышает лимит 1 МиБ.", "Plugin uploaded successfully": "Плагин успешно загружен", @@ -5947,6 +5952,7 @@ "This verification method is not allowed for this action.": "Этот способ подтверждения не разрешён для данной операции.", "This verification method requires its dedicated verification flow.": "Для этого способа необходимо пройти отдельную процедуру подтверждения.", "This version is ignored": "Уведомления об этой версии отключены", + "This version is saved but not active. Activate it to replace the current version, or activate it later from the Versions tab.": "Эта версия сохранена, но не активна. Активируйте её, чтобы заменить текущую версию, или сделайте это позже на вкладке «Версии».", "This week": "На этой неделе", "This will append 2 template rules (Codex CLI and Claude CLI) to the existing rule list.": "Это добавит 2 шаблонных правила (Codex CLI и Claude CLI) к существующему списку правил.", "This will clear custom pricing ratios and revert to upstream defaults.": "Это очистит пользовательские ценовые коэффициенты и вернет к стандартным значениям поставщика.", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index 5ee2d395c..5387378fb 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -217,6 +217,8 @@ "Actions": "Hành động", "Actions for {{name}}": "Thao tác cho {{name}}", "Activate / Roll back": "Kích hoạt / Khôi phục", + "Activate now": "Kích hoạt ngay", + "Activating...": "Đang kích hoạt...", "active": "hoạt động", "Active": "Hoạt động", "Active apps": "Ứng dụng đang hoạt động", @@ -3141,6 +3143,7 @@ "Latency check": "Kiểm tra độ trễ", "Latency short": "Trễ", "Latency trend (last 24h)": "Xu hướng độ trễ (24 giờ qua)", + "Later": "Để sau", "Later requests follow the new channel.": "Các yêu cầu sau sẽ dùng kênh mới.", "Latest platform updates and notices": "Cập nhật và thông báo nền tảng mới nhất", "Latest version": "Phiên bản mới nhất", @@ -4351,6 +4354,7 @@ "Please wait before requesting another verification code.": "Vui lòng đợi trước khi yêu cầu mã xác minh khác.", "Please wait for the current generation to complete": "Vui lòng đợi lượt tạo hiện tại hoàn tất", "Plugin": "Plugin", + "Plugin activation failed": "Kích hoạt plugin thất bại", "Plugin author": "Tác giả plugin", "Plugin default": "Mặc định của plugin", "Plugin details": "Chi tiết plugin", @@ -4365,6 +4369,7 @@ "Plugin key": "Khóa plugin", "Plugin metadata": "Siêu dữ liệu plugin", "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 uploaded successfully": "Đã tải plugin lên", @@ -5947,6 +5952,7 @@ "This verification method is not allowed for this action.": "Thao tác này không cho phép sử dụng phương thức xác minh này.", "This verification method requires its dedicated verification flow.": "Phương thức này cần được thực hiện qua quy trình xác minh riêng.", "This version is ignored": "Đã bỏ qua phiên bản này", + "This version is saved but not active. Activate it to replace the current version, or activate it later from the Versions tab.": "Phiên bản này đã được lưu nhưng chưa kích hoạt. Kích hoạt để thay thế phiên bản hiện tại, hoặc kích hoạt sau trong thẻ Phiên bản.", "This week": "Tuần này", "This will append 2 template rules (Codex CLI and Claude CLI) to the existing rule list.": "Thao tác này sẽ thêm 2 quy tắc mẫu (Codex CLI và Claude CLI) vào danh sách quy tắc hiện có.", "This will clear custom pricing ratios and revert to upstream defaults.": "Điều này sẽ xóa các tỷ lệ giá tùy chỉnh và trở về mặc định ban đầu.", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index 5ad6f54f1..784a7046f 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -217,6 +217,8 @@ "Actions": "操作", "Actions for {{name}}": "{{name}} 的操作", "Activate / Roll back": "激活 / 回滚", + "Activate now": "立即啟用", + "Activating...": "啟用中...", "active": "活躍", "Active": "生效", "Active apps": "活躍套用程式", @@ -3141,6 +3143,7 @@ "Latency check": "延遲檢測", "Latency short": "延遲", "Latency trend (last 24h)": "延遲趨勢(最近 24 小時)", + "Later": "稍後", "Later requests follow the new channel.": "後續請求改用新渠道。", "Latest platform updates and notices": "最新平台更新和通知", "Latest version": "最新版本", @@ -4351,6 +4354,7 @@ "Please wait before requesting another verification code.": "請稍後再要求驗證碼。", "Please wait for the current generation to complete": "請等待目前生成完成", "Plugin": "外掛", + "Plugin activation failed": "外掛啟用失敗", "Plugin author": "外掛作者", "Plugin default": "外掛預設值", "Plugin details": "外掛詳情", @@ -4365,6 +4369,7 @@ "Plugin key": "外掛键", "Plugin metadata": "外掛元数据", "Plugin sandbox": "外掛沙盒", + "Plugin saved, activation required": "外掛已儲存,待啟用", "Plugin source": "外掛原始碼", "Plugin source exceeds the 1 MiB limit.": "外掛原始碼超過 1 MiB 上限。", "Plugin uploaded successfully": "外掛上傳成功", @@ -5947,6 +5952,7 @@ "This verification method is not allowed for this action.": "此操作不允許使用該驗證方式。", "This verification method requires its dedicated verification flow.": "此驗證方式需要透過專用驗證流程完成。", "This version is ignored": "已忽略此版本", + "This version is saved but not active. Activate it to replace the current version, or activate it later from the Versions tab.": "此版本已儲存但尚未啟用。啟用後將取代目前版本,也可以稍後在外掛詳情的「版本」頁中啟用。", "This week": "本週", "This will append 2 template rules (Codex CLI and Claude CLI) to the existing rule list.": "這將在現有規則列表中追加 2 條模板規則(Codex CLI 和 Claude CLI)。", "This will clear custom pricing ratios and revert to upstream defaults.": "這將清除自訂定價比例並恢復到上游預設值。", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index d6ce8aaaa..2b4a5e85a 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -217,6 +217,8 @@ "Actions": "操作", "Actions for {{name}}": "{{name}} 的操作", "Activate / Roll back": "激活 / 回滚", + "Activate now": "立即激活", + "Activating...": "激活中...", "active": "活跃", "Active": "生效", "Active apps": "活跃应用", @@ -3141,6 +3143,7 @@ "Latency check": "延迟检测", "Latency short": "延迟", "Latency trend (last 24h)": "延迟趋势(最近 24 小时)", + "Later": "稍后", "Later requests follow the new channel.": "后续请求改用新渠道。", "Latest platform updates and notices": "最新平台更新和通知", "Latest version": "最新版本", @@ -4351,6 +4354,7 @@ "Please wait before requesting another verification code.": "请稍后再请求验证码。", "Please wait for the current generation to complete": "请等待当前生成完成", "Plugin": "插件", + "Plugin activation failed": "插件激活失败", "Plugin author": "插件作者", "Plugin default": "插件默认值", "Plugin details": "插件详情", @@ -4365,6 +4369,7 @@ "Plugin key": "插件键", "Plugin metadata": "插件元数据", "Plugin sandbox": "插件沙盒", + "Plugin saved, activation required": "插件已保存,待激活", "Plugin source": "插件源码", "Plugin source exceeds the 1 MiB limit.": "插件源码超过 1 MiB 上限。", "Plugin uploaded successfully": "插件上传成功", @@ -5947,6 +5952,7 @@ "This verification method is not allowed for this action.": "此操作不允许使用该验证方式。", "This verification method requires its dedicated verification flow.": "此验证方式需要通过专用验证流程完成。", "This version is ignored": "已忽略此版本", + "This version is saved but not active. Activate it to replace the current version, or activate it later from the Versions tab.": "此版本已保存但尚未激活。激活后将替换当前版本,也可以稍后在插件详情的“版本”页中激活。", "This week": "本周", "This will append 2 template rules (Codex CLI and Claude CLI) to the existing rule list.": "这将在现有规则列表中追加 2 条模板规则(Codex CLI 和 Claude CLI)。", "This will clear custom pricing ratios and revert to upstream defaults.": "这将清除自定义定价比例并恢复到上游默认值。",