mirror of
https://gh-proxy.org/https://github.com/klinsmaya/new-api.git
synced 2026-11-08 01:17:09 +08:00
fix(web): expose plugin activation after upload
This commit is contained in:
@@ -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(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<UploadDialog open={open} onOpenChange={onOpenChange} />
|
||||
<UploadDialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
initialKey={initialKey}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
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<typeof userEvent.setup>) {
|
||||
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<void>((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()
|
||||
})
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact [email protected]
|
||||
*/
|
||||
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<TaskPluginDetail | null>(null)
|
||||
const [importUrl, setImportUrl] = useState('')
|
||||
const [importError, setImportError] = useState('')
|
||||
const primaryActionRef = useRef<HTMLButtonElement>(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 (
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onOpenChange={close}
|
||||
showCloseButton={!activateMutation.isPending}
|
||||
bodyClassName='space-y-4'
|
||||
title={t('Plugin uploaded successfully')}
|
||||
description={`${result.meta.name} (${result.meta.key}) · v${result.meta.version}`}
|
||||
descriptionClassName='break-all'
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
ref={needsActivation ? undefined : primaryActionRef}
|
||||
variant={needsActivation ? 'outline' : 'default'}
|
||||
disabled={activateMutation.isPending}
|
||||
onClick={() => close(false)}
|
||||
>
|
||||
{needsActivation ? t('Later') : t('Close')}
|
||||
</Button>
|
||||
{needsActivation && (
|
||||
<Button
|
||||
ref={primaryActionRef}
|
||||
disabled={activateMutation.isPending}
|
||||
onClick={() => activateMutation.mutate(result)}
|
||||
>
|
||||
{activateMutation.isPending ? (
|
||||
<Spinner aria-hidden='true' />
|
||||
) : (
|
||||
<Power aria-hidden='true' />
|
||||
)}
|
||||
{activateMutation.isPending
|
||||
? t('Activating...')
|
||||
: t('Activate now')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Alert role='status'>
|
||||
{needsActivation ? (
|
||||
<AlertTriangle aria-hidden='true' />
|
||||
) : (
|
||||
<CircleCheck aria-hidden='true' />
|
||||
)}
|
||||
<AlertTitle>
|
||||
{needsActivation
|
||||
? t('Plugin saved, activation required')
|
||||
: t('Plugin version activated')}
|
||||
</AlertTitle>
|
||||
{needsActivation && (
|
||||
<AlertDescription>
|
||||
{t(
|
||||
'This version is saved but not active. Activate it to replace the current version, or activate it later from the Versions tab.'
|
||||
)}
|
||||
</AlertDescription>
|
||||
)}
|
||||
</Alert>
|
||||
{activateMutation.error && (
|
||||
<Alert variant='destructive'>
|
||||
<AlertTriangle aria-hidden='true' />
|
||||
<AlertTitle>{t('Plugin activation failed')}</AlertTitle>
|
||||
<AlertDescription className='break-words whitespace-pre-wrap'>
|
||||
{activateMutation.error.message}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onOpenChange={close}
|
||||
showCloseButton={!mutation.isPending}
|
||||
contentClassName='sm:max-w-3xl'
|
||||
bodyClassName='space-y-4'
|
||||
title={
|
||||
@@ -151,7 +249,11 @@ export function UploadDialog(props: UploadDialogProps) {
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<Button variant='outline' onClick={() => close(false)}>
|
||||
<Button
|
||||
variant='outline'
|
||||
disabled={mutation.isPending}
|
||||
onClick={() => close(false)}
|
||||
>
|
||||
{t('Close')}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -219,10 +321,7 @@ export function UploadDialog(props: UploadDialogProps) {
|
||||
<FieldLabel htmlFor='task-plugin-icon'>{t('Plugin icon')}</FieldLabel>
|
||||
<div className='flex items-center gap-3'>
|
||||
{icon ? (
|
||||
<PluginIcon
|
||||
plugin={{ key: result?.meta.key ?? 'upload', iconSrc: icon }}
|
||||
size={32}
|
||||
/>
|
||||
<PluginIcon plugin={{ key: 'upload', iconSrc: icon }} size={32} />
|
||||
) : null}
|
||||
<Input
|
||||
id='task-plugin-icon'
|
||||
@@ -279,16 +378,6 @@ export function UploadDialog(props: UploadDialogProps) {
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{result ? (
|
||||
<Alert>
|
||||
<CircleCheck className='text-primary' />
|
||||
<AlertTitle>{t('Parsed plugin metadata')}</AlertTitle>
|
||||
<AlertDescription className='font-mono'>
|
||||
{result.meta.key} · {result.meta.name} · v{result.meta.version}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.": "これにより、カスタム料金比率がクリアされ、上位のデフォルトに戻ります。",
|
||||
|
||||
@@ -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.": "Это очистит пользовательские ценовые коэффициенты и вернет к стандартным значениям поставщика.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.": "這將清除自訂定價比例並恢復到上游預設值。",
|
||||
|
||||
@@ -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.": "这将清除自定义定价比例并恢复到上游默认值。",
|
||||
|
||||
Reference in New Issue
Block a user