mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-11 14:41:21 +00:00
feat(web): factory task plugins update only with the system
Marketplace install/upgrade on a factory-served plugin actually created a permanent override shadowing every future built-in release. The card now shows an informational "Updates with the system" badge instead of the action, while keeping the built-in vs marketplace version line and the upgradable state badge visible. Deliberate overrides are untouched: upload and marketplace actions on overridden or third-party plugins behave as before, and the plugins table now hints when an override lags behind the shipped built-in version so operators know deleting it restores the newer factory plugin.
This commit is contained in:
@@ -26,7 +26,10 @@ import {
|
||||
GITHUB_MARKETPLACE_INDEX_URL,
|
||||
indexHasIntegrityHashes,
|
||||
isDefaultMarketplaceSource,
|
||||
isStaleFactoryOverride,
|
||||
marketplaceBuiltInVersion,
|
||||
parseMarketplaceIndex,
|
||||
resolveMarketplaceActionPolicy,
|
||||
resolvePluginSourceUrl,
|
||||
} from '../lib/marketplace'
|
||||
import type {
|
||||
@@ -52,7 +55,23 @@ function marketplacePlugin(
|
||||
}
|
||||
}
|
||||
|
||||
function installedPlugin(key: string, version: string): TaskPluginListItem {
|
||||
function factoryMeta(key: string, version: string) {
|
||||
return {
|
||||
apiVersion: 1,
|
||||
key,
|
||||
name: key,
|
||||
version,
|
||||
author: { name: 'test' as const },
|
||||
models: null,
|
||||
fetchMode: 'poll',
|
||||
}
|
||||
}
|
||||
|
||||
function installedPlugin(
|
||||
key: string,
|
||||
version: string,
|
||||
overrides: Partial<TaskPluginListItem> = {}
|
||||
): TaskPluginListItem {
|
||||
return {
|
||||
meta: {
|
||||
apiVersion: 1,
|
||||
@@ -71,6 +90,7 @@ function installedPlugin(key: string, version: string): TaskPluginListItem {
|
||||
runtime_status: 'registered',
|
||||
channel_count: 0,
|
||||
in_flight_count: 0,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,6 +419,102 @@ describe('install state derivation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('marketplace action policy', () => {
|
||||
test('factory-served plugin returns the informational system-update state', () => {
|
||||
assert.deepEqual(
|
||||
resolveMarketplaceActionPolicy(
|
||||
installedPlugin('doubao', '1.0.0', { source: 'factory' })
|
||||
),
|
||||
{ kind: 'system_update' }
|
||||
)
|
||||
})
|
||||
|
||||
test('overridden factory plugin still allows marketplace install', () => {
|
||||
assert.deepEqual(
|
||||
resolveMarketplaceActionPolicy(
|
||||
installedPlugin('doubao', '1.2.0', {
|
||||
source: 'override_over_factory',
|
||||
factory_meta: factoryMeta('doubao', '1.0.0'),
|
||||
})
|
||||
),
|
||||
{ kind: 'install' }
|
||||
)
|
||||
})
|
||||
|
||||
test('third-party plugin still allows marketplace install', () => {
|
||||
assert.deepEqual(
|
||||
resolveMarketplaceActionPolicy(installedPlugin('doubao', '1.0.0')),
|
||||
{ kind: 'install' }
|
||||
)
|
||||
})
|
||||
|
||||
test('uninstalled plugin still allows marketplace install', () => {
|
||||
assert.deepEqual(resolveMarketplaceActionPolicy(undefined), {
|
||||
kind: 'install',
|
||||
})
|
||||
})
|
||||
|
||||
test('factory-served built-in version is the installed meta version', () => {
|
||||
assert.equal(
|
||||
marketplaceBuiltInVersion(
|
||||
installedPlugin('doubao', '1.0.0', { source: 'factory' })
|
||||
),
|
||||
'1.0.0'
|
||||
)
|
||||
})
|
||||
|
||||
test('overridden factory built-in version comes from factory_meta', () => {
|
||||
assert.equal(
|
||||
marketplaceBuiltInVersion(
|
||||
installedPlugin('doubao', '1.2.0', {
|
||||
source: 'override_over_factory',
|
||||
factory_meta: factoryMeta('doubao', '1.0.0'),
|
||||
})
|
||||
),
|
||||
'1.0.0'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('stale factory override', () => {
|
||||
test('is stale when override version differs from built-in', () => {
|
||||
assert.equal(
|
||||
isStaleFactoryOverride(
|
||||
installedPlugin('doubao', '1.2.0', {
|
||||
source: 'override_over_factory',
|
||||
factory_meta: factoryMeta('doubao', '1.0.0'),
|
||||
})
|
||||
),
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
test('is not stale when override version matches built-in', () => {
|
||||
assert.equal(
|
||||
isStaleFactoryOverride(
|
||||
installedPlugin('doubao', '1.0.0', {
|
||||
source: 'override_over_factory',
|
||||
factory_meta: factoryMeta('doubao', '1.0.0'),
|
||||
})
|
||||
),
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
test('is not stale for factory-served or third-party plugins', () => {
|
||||
assert.equal(
|
||||
isStaleFactoryOverride(
|
||||
installedPlugin('doubao', '1.0.0', { source: 'factory' })
|
||||
),
|
||||
false
|
||||
)
|
||||
assert.equal(
|
||||
isStaleFactoryOverride(installedPlugin('doubao', '1.0.0')),
|
||||
false
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('marketplace version lookup', () => {
|
||||
test('finds the entry matching a version', () => {
|
||||
assert.equal(
|
||||
|
||||
@@ -29,7 +29,12 @@ import { Button } from '@/components/ui/button'
|
||||
import { getChannelTypeLabel } from '@/features/channels/lib'
|
||||
import { resolveLocalizedText } from '@/lib/localized-text'
|
||||
|
||||
import { findMarketplaceVersion, type InstallState } from '../lib/marketplace'
|
||||
import {
|
||||
findMarketplaceVersion,
|
||||
marketplaceBuiltInVersion,
|
||||
resolveMarketplaceActionPolicy,
|
||||
type InstallState,
|
||||
} from '../lib/marketplace'
|
||||
import type { MarketplacePlugin, TaskPluginListItem } from '../types'
|
||||
import { PluginIcon } from './plugin-icon'
|
||||
|
||||
@@ -47,6 +52,8 @@ export function MarketplacePluginCard(props: MarketplacePluginCardProps) {
|
||||
const channelTypes = plugin.channelTypes ?? []
|
||||
const latestEntry = findMarketplaceVersion(plugin, plugin.latest)
|
||||
const labelClass = 'text-muted-foreground text-[11px] font-medium select-none'
|
||||
const actionPolicy = resolveMarketplaceActionPolicy(props.installed)
|
||||
const builtInVersion = marketplaceBuiltInVersion(props.installed)
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col gap-2.5 rounded-xl border p-3'>
|
||||
@@ -90,12 +97,12 @@ export function MarketplacePluginCard(props: MarketplacePluginCardProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{props.installed?.factory_meta && (
|
||||
{builtInVersion && (
|
||||
<div className='text-xs'>
|
||||
<span className={labelClass}>{t('Versions')}</span>{' '}
|
||||
<span className='font-mono'>
|
||||
{t('Built-in v{{factory}} / marketplace v{{market}}', {
|
||||
factory: props.installed.factory_meta.version,
|
||||
factory: builtInVersion,
|
||||
market: plugin.latest,
|
||||
})}
|
||||
</span>
|
||||
@@ -110,17 +117,21 @@ export function MarketplacePluginCard(props: MarketplacePluginCardProps) {
|
||||
)}
|
||||
|
||||
<div className='mt-auto border-t pt-2'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant={
|
||||
props.installState.status === 'up_to_date' ? 'outline' : 'default'
|
||||
}
|
||||
className='w-full'
|
||||
onClick={props.onInstall}
|
||||
>
|
||||
<Download />
|
||||
{getActionLabel(props.installState, t)}
|
||||
</Button>
|
||||
{actionPolicy.kind === 'system_update' ? (
|
||||
<Badge variant='secondary'>{t('Updates with the system')}</Badge>
|
||||
) : (
|
||||
<Button
|
||||
size='sm'
|
||||
variant={
|
||||
props.installState.status === 'up_to_date' ? 'outline' : 'default'
|
||||
}
|
||||
className='w-full'
|
||||
onClick={props.onInstall}
|
||||
>
|
||||
<Download />
|
||||
{getActionLabel(props.installState, t)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
setTaskPluginStatus,
|
||||
TaskPluginUsageError,
|
||||
} from '../api'
|
||||
import { isStaleFactoryOverride } from '../lib/marketplace'
|
||||
import type { TaskPluginListItem, TaskPluginUsage } from '../types'
|
||||
import { PluginCard } from './plugin-card'
|
||||
import { PluginIcon } from './plugin-icon'
|
||||
@@ -158,12 +159,26 @@ export function PluginsTable(props: PluginsTableProps) {
|
||||
return <Badge variant='secondary'>{t('Factory')}</Badge>
|
||||
}
|
||||
if (row.original.source === 'override_over_factory') {
|
||||
const factoryVersion = row.original.factory_meta?.version
|
||||
const staleHint = isStaleFactoryOverride(row.original)
|
||||
? t(
|
||||
'Built-in is v{{factory}}; delete the custom version to return to it',
|
||||
{ factory: factoryVersion }
|
||||
)
|
||||
: undefined
|
||||
return (
|
||||
<Badge>
|
||||
{t('Custom (overrides factory {{version}})', {
|
||||
version: row.original.factory_meta?.version,
|
||||
})}
|
||||
</Badge>
|
||||
<div className='flex min-w-0 flex-col gap-0.5' title={staleHint}>
|
||||
<Badge>
|
||||
{t('Custom (overrides factory {{version}})', {
|
||||
version: factoryVersion,
|
||||
})}
|
||||
</Badge>
|
||||
{staleHint ? (
|
||||
<span className='text-muted-foreground text-xs'>
|
||||
{staleHint}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <Badge>{t('Third-party')}</Badge>
|
||||
|
||||
@@ -234,6 +234,46 @@ export function deriveInstallState(
|
||||
}
|
||||
}
|
||||
|
||||
export type MarketplaceActionPolicy =
|
||||
| { kind: 'install' }
|
||||
| { kind: 'system_update' }
|
||||
|
||||
/**
|
||||
* Factory-served plugins are compiled into the binary and must only update
|
||||
* with a system release. Marketplace install would create a permanent override
|
||||
* that shadows every future built-in update — that action is suppressed.
|
||||
* Overrides and third-party plugins still install/upgrade normally.
|
||||
*/
|
||||
export function resolveMarketplaceActionPolicy(
|
||||
installed?: TaskPluginListItem
|
||||
): MarketplaceActionPolicy {
|
||||
if (installed?.source === 'factory') {
|
||||
return { kind: 'system_update' }
|
||||
}
|
||||
return { kind: 'install' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Built-in version shown next to the marketplace latest. Factory-served items
|
||||
* do not carry `factory_meta` (their `meta` *is* the factory meta); overridden
|
||||
* factory plugins expose the shadowed built-in on `factory_meta`.
|
||||
*/
|
||||
export function marketplaceBuiltInVersion(
|
||||
installed?: TaskPluginListItem
|
||||
): string | undefined {
|
||||
if (!installed) return undefined
|
||||
if (installed.source === 'factory') return installed.meta.version
|
||||
return installed.factory_meta?.version
|
||||
}
|
||||
|
||||
export function isStaleFactoryOverride(item: TaskPluginListItem): boolean {
|
||||
return (
|
||||
item.source === 'override_over_factory' &&
|
||||
item.factory_meta != null &&
|
||||
item.factory_meta.version !== item.meta.version
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A source is only integrity-checked when every listed version carries a
|
||||
* sha256. Anything less and installs from it cannot be pinned, so the UI warns.
|
||||
|
||||
@@ -714,6 +714,8 @@
|
||||
"Built-in": "Built-in",
|
||||
"Built-in Device": "Built-in Device",
|
||||
"Built-in v{{factory}} / marketplace v{{market}}": "Built-in v{{factory}} / marketplace v{{market}}",
|
||||
"Updates with the system": "Updates with the system",
|
||||
"Built-in is v{{factory}}; delete the custom version to return to it": "Built-in is v{{factory}}; delete the custom version to return to it",
|
||||
"Built-in: phone fingerprint/face, or Windows Hello; External: USB security key": "Built-in: phone fingerprint/face, or Windows Hello; External: USB security key",
|
||||
"by": "by",
|
||||
"By category": "By category",
|
||||
|
||||
@@ -714,6 +714,8 @@
|
||||
"Built-in": "Intégré",
|
||||
"Built-in Device": "Appareil intégré",
|
||||
"Built-in v{{factory}} / marketplace v{{market}}": "Intégré v{{factory}} / marché v{{market}}",
|
||||
"Updates with the system": "Mise à jour système",
|
||||
"Built-in is v{{factory}}; delete the custom version to return to it": "La version intégrée est v{{factory}} ; supprimez la version personnalisée pour y revenir",
|
||||
"Built-in: phone fingerprint/face, or Windows Hello; External: USB security key": "Intégré : empreinte digitale/visage du téléphone, ou Windows Hello ; Externe : clé de sécurité USB",
|
||||
"by": "par",
|
||||
"By category": "Par catégorie",
|
||||
|
||||
@@ -714,6 +714,8 @@
|
||||
"Built-in": "組み込み",
|
||||
"Built-in Device": "内蔵デバイス",
|
||||
"Built-in v{{factory}} / marketplace v{{market}}": "組み込み v{{factory}} / マーケット v{{market}}",
|
||||
"Updates with the system": "システムとともに更新",
|
||||
"Built-in is v{{factory}}; delete the custom version to return to it": "組み込みは v{{factory}} です。カスタム版を削除すると戻ります",
|
||||
"Built-in: phone fingerprint/face, or Windows Hello; External: USB security key": "内蔵: 電話の指紋/顔認証、またはWindows Hello。外部: USBセキュリティキー",
|
||||
"by": "によって",
|
||||
"By category": "カテゴリ別",
|
||||
|
||||
@@ -714,6 +714,8 @@
|
||||
"Built-in": "Встроенный",
|
||||
"Built-in Device": "Встроенное устройство",
|
||||
"Built-in v{{factory}} / marketplace v{{market}}": "Встроенный v{{factory}} / магазин v{{market}}",
|
||||
"Updates with the system": "Обновляется с системой",
|
||||
"Built-in is v{{factory}}; delete the custom version to return to it": "Встроенная версия — v{{factory}}; удалите свою, чтобы вернуться к ней",
|
||||
"Built-in: phone fingerprint/face, or Windows Hello; External: USB security key": "Встроенное: отпечаток пальца/лицо телефона или Windows Hello; Внешнее: USB-ключ безопасности",
|
||||
"by": "от",
|
||||
"By category": "По категориям",
|
||||
|
||||
@@ -714,6 +714,8 @@
|
||||
"Built-in": "Tích hợp sẵn",
|
||||
"Built-in Device": "Thiết bị tích hợp",
|
||||
"Built-in v{{factory}} / marketplace v{{market}}": "Tích hợp v{{factory}} / chợ v{{market}}",
|
||||
"Updates with the system": "Cập nhật cùng hệ thống",
|
||||
"Built-in is v{{factory}}; delete the custom version to return to it": "Bản tích hợp là v{{factory}}; xóa bản tùy chỉnh để trở lại",
|
||||
"Built-in: phone fingerprint/face, or Windows Hello; External: USB security key": "Tích hợp sẵn: vân tay/khuôn mặt điện thoại, hoặc Windows Hello; Bên ngoài: khóa bảo mật USB",
|
||||
"by": "by",
|
||||
"By category": "Theo danh mục",
|
||||
|
||||
@@ -714,6 +714,8 @@
|
||||
"Built-in": "內置",
|
||||
"Built-in Device": "內置設備",
|
||||
"Built-in v{{factory}} / marketplace v{{market}}": "內建 v{{factory}} / 市集 v{{market}}",
|
||||
"Updates with the system": "隨系統更新",
|
||||
"Built-in is v{{factory}}; delete the custom version to return to it": "內建版本為 v{{factory}};刪除自訂版本即可恢復",
|
||||
"Built-in: phone fingerprint/face, or Windows Hello; External: USB security key": "內置:手機指紋/面部,或 Windows Hello;外部:USB 安全金鑰",
|
||||
"by": "由",
|
||||
"By category": "按行業",
|
||||
|
||||
@@ -714,6 +714,8 @@
|
||||
"Built-in": "内置",
|
||||
"Built-in Device": "内置设备",
|
||||
"Built-in v{{factory}} / marketplace v{{market}}": "内置 v{{factory}} / 市场 v{{market}}",
|
||||
"Updates with the system": "随系统更新",
|
||||
"Built-in is v{{factory}}; delete the custom version to return to it": "内置版本为 v{{factory}};删除自定义版本即可恢复",
|
||||
"Built-in: phone fingerprint/face, or Windows Hello; External: USB security key": "内置:手机指纹/面部,或 Windows Hello;外部:USB 安全密钥",
|
||||
"by": "由",
|
||||
"By category": "按行业",
|
||||
|
||||
Reference in New Issue
Block a user