fix: close plugin workspace acceptance gaps
This commit is contained in:
@@ -14,6 +14,96 @@ import {
|
||||
import type { DataServiceInstanceState } from '../../../shared/data-service';
|
||||
import type { PluginWorkspaceCommand, PluginWorkspaceItem } from './plugin-workspace-model';
|
||||
|
||||
interface DetailOperation {
|
||||
capabilityId: string;
|
||||
operationId: string;
|
||||
executionMode: 'synchronous' | 'job' | null;
|
||||
enabled: boolean | null;
|
||||
mutation: 'read' | 'write' | 'destructive' | null;
|
||||
description: string | null;
|
||||
billing: {
|
||||
mode: 'included' | 'platform_metered' | 'external_account';
|
||||
availability: 'available' | 'unavailable' | null;
|
||||
status: 'billing_unavailable' | null;
|
||||
notice: string;
|
||||
entitlementScope: string | null;
|
||||
unitName: string | null;
|
||||
unitSize: number | null;
|
||||
ratePoints: string | null;
|
||||
minimumChargePoints: string | null;
|
||||
roundingMode: 'ceil' | null;
|
||||
pricingVersion: string | number | null;
|
||||
};
|
||||
}
|
||||
|
||||
function detailOperations(item: PluginWorkspaceItem): DetailOperation[] {
|
||||
const operations = new Map<string, DetailOperation>();
|
||||
for (const operation of item.official?.detail?.operations ?? []) {
|
||||
operations.set(`${operation.capabilityId}:${operation.operation}`, {
|
||||
capabilityId: operation.capabilityId,
|
||||
operationId: operation.operation,
|
||||
executionMode: operation.executionMode,
|
||||
enabled: operation.enabled,
|
||||
mutation: null,
|
||||
description: null,
|
||||
billing: {
|
||||
...operation.billing,
|
||||
availability: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const capability of item.official?.project?.capabilities ?? []) {
|
||||
for (const operation of capability.operations) {
|
||||
const key = `${capability.id}:${operation.id}`;
|
||||
const catalogOperation = operations.get(key);
|
||||
operations.set(key, {
|
||||
capabilityId: capability.id,
|
||||
operationId: operation.id,
|
||||
executionMode: catalogOperation?.executionMode ?? null,
|
||||
enabled: catalogOperation?.enabled ?? null,
|
||||
mutation: operation.tool?.mutation ?? null,
|
||||
description: operation.tool?.description ?? null,
|
||||
billing: {
|
||||
mode: operation.billing.mode,
|
||||
availability: operation.billing.availability,
|
||||
status: null,
|
||||
notice: operation.billing.notice,
|
||||
entitlementScope: null,
|
||||
unitName: operation.billing.unitName ?? null,
|
||||
unitSize: operation.billing.unitSize ?? null,
|
||||
ratePoints: operation.billing.ratePoints ?? null,
|
||||
minimumChargePoints: operation.billing.minimumChargePoints ?? null,
|
||||
roundingMode: operation.billing.roundingMode ?? null,
|
||||
pricingVersion: operation.billing.pricingVersion ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return [...operations.values()];
|
||||
}
|
||||
|
||||
function operationGroups(operations: readonly DetailOperation[]): Array<{
|
||||
capabilityId: string;
|
||||
operations: DetailOperation[];
|
||||
}> {
|
||||
const groups = new Map<string, DetailOperation[]>();
|
||||
for (const operation of operations) {
|
||||
const group = groups.get(operation.capabilityId) ?? [];
|
||||
group.push(operation);
|
||||
groups.set(operation.capabilityId, group);
|
||||
}
|
||||
return [...groups].map(([capabilityId, groupOperations]) => ({
|
||||
capabilityId,
|
||||
operations: groupOperations,
|
||||
}));
|
||||
}
|
||||
|
||||
function operationBillingMode(mode: DetailOperation['billing']['mode']): string {
|
||||
if (mode === 'included') return '包含';
|
||||
if (mode === 'platform_metered') return '按 Token Point';
|
||||
return '外部账号';
|
||||
}
|
||||
|
||||
const SOURCE_TEXT = {
|
||||
official: '官方',
|
||||
local: '本机',
|
||||
@@ -60,6 +150,7 @@ function commandLabel(command: PluginWorkspaceCommand, title: string): string {
|
||||
case 'enable_local': return `本机全局启用${title}`;
|
||||
case 'disable_local': return `本机全局停用${title}`;
|
||||
case 'remove_local': return `移除本机包${title}`;
|
||||
case 'sign_in': return `登录后免费获取${title}`;
|
||||
case 'open_agent_assignment': return `分配${title}给伙伴`;
|
||||
case 'open_settings': return `查看${title}插件设置`;
|
||||
}
|
||||
@@ -126,6 +217,7 @@ export function PluginDetails(props: PluginDetailsProps) {
|
||||
const project = props.item.official?.project ?? null;
|
||||
const detail = props.item.official?.detail ?? null;
|
||||
const installation = props.item.official?.installation ?? null;
|
||||
const operations = detailOperations(props.item);
|
||||
const showDataService = project?.enabled
|
||||
&& project.settingsSurface === 'data-service';
|
||||
const confirmCopy = confirmation
|
||||
@@ -149,7 +241,10 @@ export function PluginDetails(props: PluginDetailsProps) {
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="secondary">{SOURCE_TEXT[props.item.source]}</Badge>
|
||||
{props.item.stale ? <Badge variant="warning">缓存</Badge> : null}
|
||||
{props.item.unavailable ? <Badge variant="warning">暂不可用</Badge> : null}
|
||||
{props.item.suspended ? <Badge variant="warning">已暂停</Badge> : null}
|
||||
{props.item.retired ? <Badge variant="warning">已退役</Badge> : null}
|
||||
{props.item.unavailable && !props.item.suspended && !props.item.retired
|
||||
? <Badge variant="warning">暂不可用</Badge> : null}
|
||||
</div>
|
||||
<DialogTitle className="text-balance text-2xl">{props.item.title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -190,6 +285,11 @@ export function PluginDetails(props: PluginDetailsProps) {
|
||||
<dd className="mt-1 text-sm font-semibold">{PROJECT_TEXT[props.item.projectState]}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{props.item.deviceReason ? (
|
||||
<p className="mt-3 rounded-xl bg-warning/10 p-3 text-pretty text-sm text-warning">
|
||||
{installation?.version ? '旧设备版本已保留' : '设备包不可用'}:{props.item.deviceReason}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="plugin-actions-heading">
|
||||
@@ -217,20 +317,38 @@ export function PluginDetails(props: PluginDetailsProps) {
|
||||
|
||||
<section aria-labelledby="plugin-capabilities-heading">
|
||||
<h3 id="plugin-capabilities-heading" className="text-lg font-semibold">能力与权限</h3>
|
||||
{project?.capabilities.length ? (
|
||||
{operations.length ? (
|
||||
<div className="mt-3 space-y-3">
|
||||
{project.capabilities.map((capability) => (
|
||||
<article key={capability.id} className="rounded-xl border border-border/70 p-4">
|
||||
<h4 className="font-mono text-sm font-semibold">{capability.id}</h4>
|
||||
{operationGroups(operations).map((capability) => (
|
||||
<article key={capability.capabilityId} className="rounded-xl border border-border/70 p-4">
|
||||
<h4 className="font-mono text-sm font-semibold">{capability.capabilityId}</h4>
|
||||
<ul className="mt-3 space-y-3">
|
||||
{capability.operations.map((operation) => (
|
||||
<li key={operation.id} className="rounded-lg bg-surface-subtle p-3 text-sm">
|
||||
<li key={operation.operationId} className="rounded-lg bg-surface-subtle p-3 text-sm">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<code className="font-semibold">{operation.id}</code>
|
||||
{operation.tool ? <Badge variant="outline">{operation.tool.mutation}</Badge> : <Badge variant="outline">SDK</Badge>}
|
||||
<code className="font-semibold">{operation.operationId}</code>
|
||||
{operation.mutation
|
||||
? <Badge variant="outline">{operation.mutation}</Badge>
|
||||
: operation.executionMode
|
||||
? <Badge variant="outline">{operation.executionMode === 'job' ? '异步任务' : '同步'}</Badge>
|
||||
: <Badge variant="outline">SDK</Badge>}
|
||||
{operation.enabled === false ? <Badge variant="warning">服务端已停用</Badge> : null}
|
||||
{operation.billing.availability === 'unavailable'
|
||||
|| operation.billing.status === 'billing_unavailable'
|
||||
? <Badge variant="warning">计费暂不可用</Badge> : null}
|
||||
</div>
|
||||
{operation.tool ? <p className="mt-2 text-pretty text-muted-foreground">{operation.tool.description}</p> : null}
|
||||
{operation.description ? <p className="mt-2 text-pretty text-muted-foreground">{operation.description}</p> : null}
|
||||
<p className="mt-2 text-xs">{operation.billing.notice}</p>
|
||||
<dl className="mt-2 grid gap-x-4 gap-y-1 text-xs text-muted-foreground sm:grid-cols-2">
|
||||
<div><dt className="inline">计费方式:</dt><dd className="inline">{operationBillingMode(operation.billing.mode)}</dd></div>
|
||||
{operation.billing.entitlementScope ? <div><dt className="inline">权益范围:</dt><dd className="inline">{operation.billing.entitlementScope}</dd></div> : null}
|
||||
{operation.billing.unitName ? <div><dt className="inline">计费单位:</dt><dd className="inline">{operation.billing.unitName}</dd></div> : null}
|
||||
{operation.billing.unitSize !== null ? <div><dt className="inline">单位大小:</dt><dd className="inline">{operation.billing.unitSize}</dd></div> : null}
|
||||
{operation.billing.ratePoints !== null ? <div><dt className="inline">每单位 Token Point:</dt><dd className="inline">{operation.billing.ratePoints}</dd></div> : null}
|
||||
{operation.billing.minimumChargePoints !== null ? <div><dt className="inline">最低扣点:</dt><dd className="inline">{operation.billing.minimumChargePoints}</dd></div> : null}
|
||||
{operation.billing.pricingVersion !== null ? <div><dt className="inline">价格版本:</dt><dd className="inline">{operation.billing.pricingVersion}</dd></div> : null}
|
||||
{operation.billing.roundingMode === 'ceil' ? <div><dt className="inline">舍入方式:</dt><dd className="inline">向上取整</dd></div> : null}
|
||||
</dl>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, type FormEvent } from 'react';
|
||||
import { AlertCircle, Puzzle, RefreshCw, Search } from 'lucide-react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -22,7 +22,7 @@ import type {
|
||||
PluginWorkspaceState,
|
||||
} from './plugin-workspace-model';
|
||||
import { buildPluginWorkspaceProjection } from './plugin-workspace-model';
|
||||
import { parsePluginWorkspaceSearch, serializePluginWorkspaceSearch } from './plugin-workspace-query';
|
||||
import { resolvePluginWorkspaceSearch, serializePluginWorkspaceSearch } from './plugin-workspace-query';
|
||||
|
||||
const DELIVERY_TEXT = {
|
||||
system_included: '随 MakeLore 提供',
|
||||
@@ -102,7 +102,9 @@ function PluginCard({
|
||||
<Badge variant={item.source === 'local' ? 'secondary' : 'outline'}>{cardSource(item)}</Badge>
|
||||
<Badge variant="outline">{DELIVERY_TEXT[item.delivery]}</Badge>
|
||||
{item.updateAvailable ? <Badge variant="success">可更新</Badge> : null}
|
||||
{item.unavailable ? <Badge variant="warning">不可用</Badge> : null}
|
||||
{item.suspended ? <Badge variant="warning">已暂停</Badge> : null}
|
||||
{item.retired ? <Badge variant="warning">已退役</Badge> : null}
|
||||
{item.unavailable && !item.suspended && !item.retired ? <Badge variant="warning">不可用</Badge> : null}
|
||||
{item.stale ? <Badge variant="warning">缓存</Badge> : null}
|
||||
</div>
|
||||
<Title className="mt-4 text-balance text-xl font-semibold">{item.title}</Title>
|
||||
@@ -113,6 +115,11 @@ function PluginCard({
|
||||
<div className="flex justify-between gap-4"><dt className="text-muted-foreground">伙伴</dt><dd className="text-right font-medium">{agentText(item)}</dd></div>
|
||||
<div className="flex justify-between gap-4"><dt className="text-muted-foreground">计费</dt><dd className="text-right font-medium">{BILLING_TEXT[item.billing]}</dd></div>
|
||||
</dl>
|
||||
{item.deviceReason ? (
|
||||
<p className="mt-3 text-pretty text-xs leading-5 text-warning">
|
||||
{item.official?.installation?.version ? '旧设备版本已保留' : '设备包不可用'}:{item.deviceReason}
|
||||
</p>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -338,10 +345,16 @@ function isSettled(state: 'idle' | 'loading' | 'ready' | 'error'): boolean {
|
||||
return state === 'ready' || state === 'error';
|
||||
}
|
||||
|
||||
function scopeFallbackSearchFromLocationState(state: unknown): string | null {
|
||||
if (!state || typeof state !== 'object') return null;
|
||||
const value = (state as Record<string, unknown>).pluginWorkspaceScopeFallback;
|
||||
return typeof value === 'string' ? value : null;
|
||||
}
|
||||
|
||||
export function Plugins() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const canonicalized = useRef(false);
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const accountKey = user?.userId ?? user?.username ?? null;
|
||||
const activeProject = useCodingWorkspaceStore((state) => state.activeProject);
|
||||
@@ -372,10 +385,15 @@ export function Plugins() {
|
||||
|
||||
const workspaceHydrated = workspaceLoadState === 'ready' || workspaceLoadState === 'error';
|
||||
const queryString = searchParams.toString();
|
||||
const filters = useMemo(
|
||||
() => parsePluginWorkspaceSearch(new URLSearchParams(queryString), Boolean(activeProject)),
|
||||
const searchResolution = useMemo(
|
||||
() => resolvePluginWorkspaceSearch(new URLSearchParams(queryString), Boolean(activeProject)),
|
||||
[activeProject, queryString],
|
||||
);
|
||||
const fallbackLocationSearch = scopeFallbackSearchFromLocationState(location.state);
|
||||
const scopeFallbackNotice = searchResolution.scopeFallback || (
|
||||
!activeProject && fallbackLocationSearch === queryString
|
||||
);
|
||||
const filters = searchResolution.filters;
|
||||
const currentProjectProjection = activeProject
|
||||
&& projectProjection?.project.localProjectId === activeProject.id
|
||||
? projectProjection
|
||||
@@ -384,7 +402,7 @@ export function Plugins() {
|
||||
() => Object.fromEntries((workspaceConfig?.agents ?? []).map((agent) => [agent.id, agent.name || agent.roleName])),
|
||||
[workspaceConfig],
|
||||
);
|
||||
const projection = useMemo(() => buildPluginWorkspaceProjection({
|
||||
const baseProjection = useMemo(() => buildPluginWorkspaceProjection({
|
||||
catalog,
|
||||
details,
|
||||
library,
|
||||
@@ -393,25 +411,65 @@ export function Plugins() {
|
||||
project: currentProjectProjection,
|
||||
activeProject: activeProject ? { id: activeProject.id, name: activeProject.name } : null,
|
||||
agentNames,
|
||||
authenticated: Boolean(accountKey),
|
||||
sourceFailures: {
|
||||
catalog: catalogState === 'error',
|
||||
library: Boolean(accountKey) && libraryState === 'error',
|
||||
},
|
||||
filters,
|
||||
}), [
|
||||
activeProject,
|
||||
agentNames,
|
||||
catalog,
|
||||
catalogState,
|
||||
currentProjectProjection,
|
||||
details,
|
||||
devicePackages,
|
||||
filters,
|
||||
installations,
|
||||
library,
|
||||
libraryState,
|
||||
accountKey,
|
||||
]);
|
||||
const projection = useMemo<PluginWorkspaceProjection>(() => {
|
||||
if (!scopeFallbackNotice && !searchResolution.scopeFallback) return baseProjection;
|
||||
return {
|
||||
...baseProjection,
|
||||
notices: [
|
||||
{ kind: 'scope_fallback', message: '当前没有打开的项目,已显示全部插件。' },
|
||||
...baseProjection.notices.filter((notice) => notice.kind !== 'scope_fallback'),
|
||||
],
|
||||
};
|
||||
}, [baseProjection, scopeFallbackNotice, searchResolution.scopeFallback]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspaceHydrated || canonicalized.current) return;
|
||||
canonicalized.current = true;
|
||||
const canonical = serializePluginWorkspaceSearch(filters);
|
||||
if (canonical.toString() !== queryString) setSearchParams(canonical, { replace: true });
|
||||
}, [filters, queryString, setSearchParams, workspaceHydrated]);
|
||||
if (!workspaceHydrated) return;
|
||||
const nextFallbackSearch = searchResolution.scopeFallback
|
||||
? searchResolution.canonicalSearch
|
||||
: null;
|
||||
const mustClearFallbackState = fallbackLocationSearch !== null
|
||||
&& (Boolean(activeProject) || queryString !== fallbackLocationSearch);
|
||||
if (
|
||||
searchResolution.canonicalSearch !== queryString
|
||||
|| fallbackLocationSearch !== nextFallbackSearch && (
|
||||
searchResolution.scopeFallback || mustClearFallbackState
|
||||
)
|
||||
) {
|
||||
setSearchParams(new URLSearchParams(searchResolution.canonicalSearch), {
|
||||
replace: true,
|
||||
state: nextFallbackSearch
|
||||
? { pluginWorkspaceScopeFallback: nextFallbackSearch }
|
||||
: null,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
activeProject,
|
||||
fallbackLocationSearch,
|
||||
queryString,
|
||||
searchResolution,
|
||||
setSearchParams,
|
||||
workspaceHydrated,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspaceHydrated) return;
|
||||
@@ -500,7 +558,7 @@ export function Plugins() {
|
||||
}, [accountKey, activeProject, catalogState, detailState, deviceState, libraryState, projectLoadState, selectedOfficialId]);
|
||||
|
||||
const setFilters = (next: PluginWorkspaceFilters) => {
|
||||
setSearchParams(serializePluginWorkspaceSearch(next));
|
||||
setSearchParams(serializePluginWorkspaceSearch(next), { state: null });
|
||||
};
|
||||
const select = (key: PluginWorkspaceItem['key'] | null) => {
|
||||
setFilters({ ...filters, selectedKey: key });
|
||||
@@ -526,6 +584,7 @@ export function Plugins() {
|
||||
case 'enable_local':
|
||||
case 'disable_local': return Boolean(devicePending[`enabled:${command.packageId}`]);
|
||||
case 'remove_local': return Boolean(devicePending[`uninstall:${command.packageId}`]);
|
||||
case 'sign_in':
|
||||
case 'open_agent_assignment':
|
||||
case 'open_settings': return false;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ export async function dispatchPluginWorkspaceCommand(
|
||||
case 'remove_local':
|
||||
await dependencies.device.uninstall(command.packageId);
|
||||
return;
|
||||
case 'sign_in':
|
||||
dependencies.navigate('/login');
|
||||
return;
|
||||
case 'open_agent_assignment':
|
||||
dependencies.navigate('/project-config');
|
||||
return;
|
||||
|
||||
@@ -47,6 +47,7 @@ export type PluginWorkspaceCommand =
|
||||
| { kind: 'enable_local'; packageId: string }
|
||||
| { kind: 'disable_local'; packageId: string }
|
||||
| { kind: 'remove_local'; packageId: string }
|
||||
| { kind: 'sign_in' }
|
||||
| { kind: 'open_agent_assignment'; projectId: string; pluginId: string }
|
||||
| { kind: 'open_settings'; projectId: string; pluginId: string };
|
||||
|
||||
@@ -94,6 +95,9 @@ export interface PluginWorkspaceItem {
|
||||
updateAvailable: boolean;
|
||||
unavailable: boolean;
|
||||
stale: boolean;
|
||||
suspended: boolean;
|
||||
retired: boolean;
|
||||
deviceReason: string | null;
|
||||
commands: readonly PluginWorkspaceCommand[];
|
||||
official: PluginWorkspaceOfficialSources | null;
|
||||
local: DevicePackageRecordV1 | null;
|
||||
@@ -113,6 +117,8 @@ export interface PluginWorkspaceInputs {
|
||||
project: CodingPluginProject | null;
|
||||
activeProject: { id: string; name: string } | null;
|
||||
agentNames: Readonly<Record<string, string>>;
|
||||
authenticated: boolean;
|
||||
sourceFailures?: Readonly<{ catalog?: boolean; library?: boolean }>;
|
||||
filters: PluginWorkspaceFilters;
|
||||
}
|
||||
|
||||
@@ -157,6 +163,14 @@ function currentLibraryEntry(entry: MarketplaceLibraryEntry | null): boolean {
|
||||
}
|
||||
|
||||
function installed(installation: MarketplaceInstallation | null): boolean {
|
||||
return installation?.status === 'installed'
|
||||
|| installation?.status === 'kept'
|
||||
|| (installation?.status === 'unavailable'
|
||||
&& Boolean(installation.version)
|
||||
&& Boolean(installation.channel));
|
||||
}
|
||||
|
||||
function installationReady(installation: MarketplaceInstallation | null): boolean {
|
||||
return installation?.status === 'installed' || installation?.status === 'kept';
|
||||
}
|
||||
|
||||
@@ -182,8 +196,10 @@ function officialCommands(
|
||||
input: PluginWorkspaceInputs,
|
||||
): PluginWorkspaceCommand[] {
|
||||
const commands: PluginWorkspaceCommand[] = [];
|
||||
const libraryCurrent = currentLibraryEntry(record.library);
|
||||
const removed = record.library?.removedAt !== null && record.library?.removedAt !== undefined;
|
||||
const libraryCurrent = input.authenticated && currentLibraryEntry(record.library);
|
||||
const removed = input.authenticated
|
||||
&& record.library?.removedAt !== null
|
||||
&& record.library?.removedAt !== undefined;
|
||||
const systemIncluded = record.library?.acquisitionMode === 'system_included'
|
||||
|| record.catalog?.acquisition === 'system_included';
|
||||
const bundledDelivery = systemIncluded || isCodeOwnedOptionalBundledPluginId(record.pluginId);
|
||||
@@ -193,9 +209,12 @@ function officialCommands(
|
||||
const hasDevicePackage = !bundledDelivery && installed(record.installation);
|
||||
const deliveryReady = systemIncluded
|
||||
|| (bundledDelivery && libraryCurrent)
|
||||
|| (libraryCurrent && hasDevicePackage);
|
||||
|| (libraryCurrent && !bundledDelivery && installationReady(record.installation));
|
||||
|
||||
if (!systemIncluded && libraryCurrent) {
|
||||
if (!systemIncluded && !input.authenticated
|
||||
&& record.catalog?.acquisition === 'free' && sourceAvailable) {
|
||||
commands.push({ kind: 'sign_in' });
|
||||
} else if (!systemIncluded && libraryCurrent) {
|
||||
commands.push({ kind: 'remove_from_library', pluginId: record.pluginId });
|
||||
} else if (!systemIncluded && removed && sourceAvailable) {
|
||||
commands.push({ kind: 'reacquire', pluginId: record.pluginId });
|
||||
@@ -246,6 +265,9 @@ function officialItem(
|
||||
const systemIncluded = record.library?.acquisitionMode === 'system_included'
|
||||
|| record.catalog?.acquisition === 'system_included';
|
||||
const current = currentLibraryEntry(record.library);
|
||||
const suspended = record.catalog?.runtimeStatus === 'suspended'
|
||||
|| record.library?.runtimeStatus === 'suspended';
|
||||
const retired = record.library?.catalogStatus === 'retired';
|
||||
const projectState: PluginWorkspaceProjectState = !input.activeProject
|
||||
? 'not_applicable'
|
||||
: !record.project
|
||||
@@ -266,23 +288,31 @@ function officialItem(
|
||||
version: record.installation?.version ?? record.project?.version ?? base?.stableVersion ?? null,
|
||||
delivery: systemIncluded
|
||||
? 'system_included'
|
||||
: current
|
||||
: input.authenticated && current
|
||||
? 'account_acquired'
|
||||
: record.library?.removedAt
|
||||
: input.authenticated && record.library?.removedAt
|
||||
? 'account_removed'
|
||||
: input.library ? 'not_acquired' : 'account_unknown',
|
||||
: input.authenticated && input.library ? 'not_acquired' : 'account_unknown',
|
||||
projectState,
|
||||
localEnabled: null,
|
||||
assignedAgentIds: agents.ids,
|
||||
assignedAgentNames: agents.names,
|
||||
billing: record.detail?.usageBilling ?? record.catalog?.usageBilling ?? 'unknown',
|
||||
updateAvailable: updateAvailable(record),
|
||||
unavailable: record.catalog?.runtimeStatus === 'suspended'
|
||||
|| record.library?.runtimeStatus === 'suspended'
|
||||
|| record.library?.catalogStatus === 'retired'
|
||||
unavailable: suspended
|
||||
|| retired
|
||||
|| record.installation?.status === 'unavailable'
|
||||
|| record.project?.state === 'unavailable',
|
||||
stale: Boolean(record.detail?.stale || input.catalog?.stale || input.library?.stale),
|
||||
stale: Boolean(
|
||||
record.detail?.stale
|
||||
|| input.catalog?.stale
|
||||
|| input.library?.stale
|
||||
|| (record.catalog && input.sourceFailures?.catalog)
|
||||
|| (record.library && input.sourceFailures?.library)
|
||||
),
|
||||
suspended,
|
||||
retired,
|
||||
deviceReason: record.installation?.reason ?? null,
|
||||
commands: officialCommands(record, input),
|
||||
official: {
|
||||
catalog: record.catalog,
|
||||
@@ -316,6 +346,9 @@ function localItem(local: DevicePackageRecordV1): PluginWorkspaceItem {
|
||||
updateAvailable: false,
|
||||
unavailable: false,
|
||||
stale: false,
|
||||
suspended: false,
|
||||
retired: false,
|
||||
deviceReason: null,
|
||||
commands: [
|
||||
{ kind: local.enabled ? 'disable_local' : 'enable_local', packageId: local.packageId },
|
||||
{ kind: 'remove_local', packageId: local.packageId },
|
||||
@@ -327,32 +360,53 @@ function localItem(local: DevicePackageRecordV1): PluginWorkspaceItem {
|
||||
|
||||
function retainedItem(
|
||||
pluginId: string,
|
||||
activeProject: PluginWorkspaceInputs['activeProject'],
|
||||
projectId: string | null,
|
||||
record: OfficialAccumulator | null,
|
||||
input: PluginWorkspaceInputs,
|
||||
): PluginWorkspaceItem {
|
||||
const base = record?.detail ?? record?.catalog ?? record?.library;
|
||||
const suspended = record?.catalog?.runtimeStatus === 'suspended'
|
||||
|| record?.library?.runtimeStatus === 'suspended';
|
||||
const retired = record?.library?.catalogStatus === 'retired';
|
||||
return {
|
||||
key: `retained:${pluginId}`,
|
||||
source: 'retained',
|
||||
pluginId,
|
||||
packageId: null,
|
||||
title: pluginId,
|
||||
summary: '项目配置仍保留此插件 ID,但当前来源无法解释它。',
|
||||
category: null,
|
||||
tags: [],
|
||||
publisher: null,
|
||||
version: null,
|
||||
title: base?.title ?? pluginId,
|
||||
summary: base?.summary ?? '项目配置仍保留此插件 ID,但当前项目投影无法解释它。',
|
||||
category: base?.category ?? null,
|
||||
tags: record?.detail?.tags ?? record?.catalog?.tags ?? [],
|
||||
publisher: record?.detail?.providerDisplayName ?? record?.catalog?.providerDisplayName ?? null,
|
||||
version: record?.installation?.version ?? base?.stableVersion ?? null,
|
||||
delivery: 'retained',
|
||||
projectState: 'unavailable',
|
||||
localEnabled: null,
|
||||
assignedAgentIds: [],
|
||||
assignedAgentNames: [],
|
||||
billing: 'unknown',
|
||||
updateAvailable: false,
|
||||
billing: record?.detail?.usageBilling ?? record?.catalog?.usageBilling ?? 'unknown',
|
||||
updateAvailable: record ? updateAvailable(record) : false,
|
||||
unavailable: true,
|
||||
stale: false,
|
||||
commands: activeProject
|
||||
? [{ kind: 'disable_project', projectId: activeProject.id, pluginId }]
|
||||
stale: Boolean(
|
||||
record?.detail?.stale
|
||||
|| (record?.catalog && input.catalog?.stale)
|
||||
|| (record?.library && input.library?.stale)
|
||||
|| (record?.catalog && input.sourceFailures?.catalog)
|
||||
|| (record?.library && input.sourceFailures?.library)
|
||||
),
|
||||
suspended,
|
||||
retired,
|
||||
deviceReason: record?.installation?.reason ?? null,
|
||||
commands: projectId
|
||||
? [{ kind: 'disable_project', projectId, pluginId }]
|
||||
: [],
|
||||
official: null,
|
||||
official: record ? {
|
||||
catalog: record.catalog,
|
||||
detail: record.detail,
|
||||
library: record.library,
|
||||
installation: record.installation,
|
||||
project: record.project,
|
||||
} : null,
|
||||
local: null,
|
||||
};
|
||||
}
|
||||
@@ -428,8 +482,13 @@ export function buildPluginWorkspaceProjection(input: PluginWorkspaceInputs): Pl
|
||||
.sort((left, right) => left.displayName.localeCompare(right.displayName))
|
||||
.map(localItem);
|
||||
const retainedItems = orderedUnique(input.project?.unknownPluginIds ?? [])
|
||||
.filter((pluginId) => pluginId !== NATIVE_WEB_SEARCH_ID && !records.has(pluginId))
|
||||
.map((pluginId) => retainedItem(pluginId, input.activeProject));
|
||||
.filter((pluginId) => pluginId !== NATIVE_WEB_SEARCH_ID && !records.get(pluginId)?.project)
|
||||
.map((pluginId) => retainedItem(
|
||||
pluginId,
|
||||
input.project?.project.localProjectId ?? null,
|
||||
records.get(pluginId) ?? null,
|
||||
input,
|
||||
));
|
||||
const scopedItems = [...officialItems, ...localItems, ...retainedItems];
|
||||
const sourceAndSearchItems = scopedItems.filter((item) => {
|
||||
const sourceMatches = input.filters.source === 'all' || item.source === input.filters.source;
|
||||
|
||||
@@ -52,3 +52,21 @@ export function serializePluginWorkspaceSearch(filters: PluginWorkspaceFilters):
|
||||
if (filters.selectedKey) search.set('plugin', filters.selectedKey);
|
||||
return search;
|
||||
}
|
||||
|
||||
export interface PluginWorkspaceSearchResolution {
|
||||
filters: PluginWorkspaceFilters;
|
||||
canonicalSearch: string;
|
||||
scopeFallback: boolean;
|
||||
}
|
||||
|
||||
export function resolvePluginWorkspaceSearch(
|
||||
search: URLSearchParams,
|
||||
hasActiveProject: boolean,
|
||||
): PluginWorkspaceSearchResolution {
|
||||
const filters = parsePluginWorkspaceSearch(search, hasActiveProject);
|
||||
return {
|
||||
filters,
|
||||
canonicalSearch: serializePluginWorkspaceSearch(filters).toString(),
|
||||
scopeFallback: search.get('scope') === 'project' && !hasActiveProject,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user