mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-12 07:00:42 +00:00
feat(task): replace built-in task adaptors with a sandboxed JS plugin system (#7076)
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
Copyright (C) 2023-2026 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
import {
|
||||
resolveLocalizedText,
|
||||
type LocalizedTextValue,
|
||||
} from '../localized-text'
|
||||
|
||||
type ResolveCase = {
|
||||
name: string
|
||||
value: LocalizedTextValue | null | undefined
|
||||
language: string
|
||||
expected: string
|
||||
}
|
||||
|
||||
const KLING = {
|
||||
en: 'Video generation via Kling API',
|
||||
zh: '可灵视频生成',
|
||||
'zh-TW': '可靈影片生成',
|
||||
} as const
|
||||
|
||||
describe('resolveLocalizedText', () => {
|
||||
test.each<ResolveCase>([
|
||||
{
|
||||
name: 'returns a bare string unchanged so legacy marketplace indexes still render',
|
||||
value: 'Video generation via Kling API',
|
||||
language: 'zhCN',
|
||||
expected: 'Video generation via Kling API',
|
||||
},
|
||||
{
|
||||
name: 'returns the exact BCP-47 tag when the map contains zh-TW',
|
||||
value: KLING,
|
||||
language: 'zh-TW',
|
||||
expected: '可靈影片生成',
|
||||
},
|
||||
{
|
||||
name: 'matches zh-TW case-insensitively when i18next language is zh-tw',
|
||||
value: KLING,
|
||||
language: 'zh-tw',
|
||||
expected: '可靈影片生成',
|
||||
},
|
||||
{
|
||||
name: 'maps the project i18next code zhTW onto the zh-TW map key',
|
||||
value: KLING,
|
||||
language: 'zhTW',
|
||||
expected: '可靈影片生成',
|
||||
},
|
||||
{
|
||||
name: 'falls back from zh-TW to the zh primary subtag when zh-TW is absent',
|
||||
value: { en: KLING.en, zh: KLING.zh },
|
||||
language: 'zh-TW',
|
||||
expected: '可灵视频生成',
|
||||
},
|
||||
{
|
||||
name: 'maps the project i18next code zhCN onto the zh primary subtag',
|
||||
value: { en: KLING.en, zh: KLING.zh },
|
||||
language: 'zhCN',
|
||||
expected: '可灵视频生成',
|
||||
},
|
||||
{
|
||||
name: 'falls back from en-US to en when only the primary tag exists',
|
||||
value: { en: KLING.en, zh: KLING.zh },
|
||||
language: 'en-US',
|
||||
expected: 'Video generation via Kling API',
|
||||
},
|
||||
{
|
||||
name: 'falls back to en when the requested language and its primary tag are absent',
|
||||
value: { en: KLING.en, ja: 'Kling で動画生成' },
|
||||
language: 'fr',
|
||||
expected: 'Video generation via Kling API',
|
||||
},
|
||||
{
|
||||
name: 'uses the first sorted key when en and the requested language are both absent',
|
||||
value: { ja: 'Kling で動画生成', fr: 'Génération vidéo Kling' },
|
||||
language: 'ru',
|
||||
expected: 'Génération vidéo Kling',
|
||||
},
|
||||
{
|
||||
name: 'returns an empty string when the value is null',
|
||||
value: null,
|
||||
language: 'en',
|
||||
expected: '',
|
||||
},
|
||||
{
|
||||
name: 'returns an empty string when the value is undefined',
|
||||
value: undefined,
|
||||
language: 'en',
|
||||
expected: '',
|
||||
},
|
||||
{
|
||||
name: 'returns an empty string when the map has no usable entries',
|
||||
value: {},
|
||||
language: 'zhCN',
|
||||
expected: '',
|
||||
},
|
||||
])('$name', ({ value, language, expected }) => {
|
||||
expect(resolveLocalizedText(value, language)).toBe(expected)
|
||||
})
|
||||
})
|
||||
@@ -25,6 +25,7 @@ export type AdminCapabilities = AdminPermissionMatrix
|
||||
|
||||
export const ADMIN_PERMISSION_RESOURCES = {
|
||||
CHANNEL: 'channel',
|
||||
TASK_PLUGIN: 'task_plugin',
|
||||
} as const
|
||||
|
||||
export const ADMIN_PERMISSION_ACTIONS = {
|
||||
@@ -33,6 +34,7 @@ export const ADMIN_PERMISSION_ACTIONS = {
|
||||
WRITE: 'write',
|
||||
SENSITIVE_WRITE: 'sensitive_write',
|
||||
SECRET_VIEW: 'secret_view',
|
||||
BIND: 'bind',
|
||||
} as const
|
||||
|
||||
// The role whose baseline grants are used as defaults in the permission editor.
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright (C) 2023-2026 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
/**
|
||||
* Plugin / marketplace copy that may be a bare string (legacy marketplace
|
||||
* index) or a BCP-47 map. Gateway APIs always emit the map form.
|
||||
*/
|
||||
export type LocalizedTextValue = string | Record<string, string>
|
||||
|
||||
/**
|
||||
* Resolve LocalizedText against an i18next language code.
|
||||
*
|
||||
* This project's `i18n.language` values are `en` / `zhCN` / `zhTW` / `fr` /
|
||||
* `ru` / `ja` / `vi` (see `web/src/i18n/config.ts`). Backend keys are BCP-47
|
||||
* (`en`, `zh`, `zh-TW`). Matching is case-insensitive and also accepts
|
||||
* hyphenated tags (`zh-TW`, `en-US`) so callers can pass either shape.
|
||||
*
|
||||
* Fallback: exact tag → primary subtag → `en` → first key in sorted order → `''`.
|
||||
*/
|
||||
export function resolveLocalizedText(
|
||||
value: LocalizedTextValue | undefined | null,
|
||||
language: string
|
||||
): string {
|
||||
if (value == null) return ''
|
||||
if (typeof value === 'string') return value
|
||||
if (typeof value !== 'object' || Array.isArray(value)) return ''
|
||||
|
||||
const texts = new Map<string, string>()
|
||||
for (const [key, text] of Object.entries(value)) {
|
||||
if (typeof text !== 'string' || text.trim() === '') continue
|
||||
const locale = key.trim().replaceAll('_', '-').toLowerCase()
|
||||
if (!locale) continue
|
||||
texts.set(locale, text)
|
||||
}
|
||||
if (texts.size === 0) return ''
|
||||
|
||||
for (const candidate of localeFallbackKeys(language)) {
|
||||
const hit = texts.get(candidate)
|
||||
if (hit !== undefined) return hit
|
||||
}
|
||||
|
||||
const firstKey = [...texts.keys()].sort((left, right) =>
|
||||
left.localeCompare(right)
|
||||
)[0]
|
||||
return firstKey ? (texts.get(firstKey) ?? '') : ''
|
||||
}
|
||||
|
||||
function localeFallbackKeys(language: string): string[] {
|
||||
const normalized = language.trim().replaceAll('_', '-').toLowerCase()
|
||||
const keys: string[] = []
|
||||
const add = (tag: string) => {
|
||||
if (tag && !keys.includes(tag)) keys.push(tag)
|
||||
}
|
||||
|
||||
add(normalized)
|
||||
if (normalized === 'zhcn') add('zh-cn')
|
||||
if (normalized === 'zhtw') add('zh-tw')
|
||||
|
||||
if (normalized.includes('-')) {
|
||||
add(normalized.slice(0, normalized.indexOf('-')))
|
||||
} else if (normalized === 'zhcn' || normalized === 'zhtw') {
|
||||
add('zh')
|
||||
}
|
||||
add('en')
|
||||
return keys
|
||||
}
|
||||
Reference in New Issue
Block a user