fix(plugins): close R2 workspace gaps

This commit is contained in:
2026-09-03 15:07:29 +08:00
parent c5020ae22c
commit 0bfabc0df2
10 changed files with 405 additions and 15 deletions

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useRef, useState, type FormEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { Loader2, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -15,6 +15,7 @@ const LOGIN_ERROR_MESSAGE = '登录失败,请稍后重试。';
const CAPTCHA_ERROR_MESSAGE = '图形验证码加载失败,请重试。';
const SMS_ERROR_MESSAGE = '验证码发送失败,请稍后重试。';
const PHONE_PATTERN = /^1\d{10}$/;
const DEFAULT_LOGIN_RETURN_PATH = '/module-select';
type LoginMode = 'password' | 'mobile';
@@ -101,8 +102,21 @@ function getLoginErrorMessage(error: string | null): string | null {
return /[\u3400-\u9fff]/.test(message) ? message : LOGIN_ERROR_MESSAGE;
}
function getSafeLoginReturnPath(state: unknown): string {
if (!state || typeof state !== 'object') return DEFAULT_LOGIN_RETURN_PATH;
const value = Reflect.get(state, 'from');
return typeof value === 'string'
&& value.startsWith('/')
&& value[1] !== '/'
&& value[1] !== '\\'
? value
: DEFAULT_LOGIN_RETURN_PATH;
}
export function Login() {
const navigate = useNavigate();
const location = useLocation();
const returnPath = getSafeLoginReturnPath(location.state);
const loginWithPassword = useAuthStore((state) => state.loginWithPassword);
const loginWithMobile = useAuthStore((state) => state.loginWithMobile);
const loading = useAuthStore((state) => state.loading);
@@ -240,9 +254,9 @@ export function Login() {
useEffect(() => {
if (isAuthenticated() && !submitting) {
navigate('/module-select', { replace: true });
navigate(returnPath, { replace: true });
}
}, [isAuthenticated, navigate, submitting]);
}, [isAuthenticated, navigate, returnPath, submitting]);
const finishLogin = async (login: () => Promise<void>) => {
setSubmitError(null);
@@ -257,7 +271,7 @@ export function Login() {
await logout();
throw syncError;
}
navigate('/module-select', { replace: true });
navigate(returnPath, { replace: true });
} catch (loginError) {
setSubmitError(loginError instanceof Error ? loginError.message : String(loginError));
} finally {

View File

@@ -128,6 +128,11 @@ const PROJECT_TEXT = {
not_applicable: '不属于项目启用范围',
} as const;
const PROJECT_POLICY_NOTICE = {
stale: '当前项目策略使用缓存,所示能力与价格可能不是最新状态。',
unavailable: '当前项目策略不可用,无法确认最新能力与价格。',
} as const;
function billingText(item: PluginWorkspaceItem): string {
if (item.billing === 'included') return '按平台包含,不按单次插件调用扣点。';
if (item.billing === 'token_point') return '按 Token Point 实际用量计费,价格与回执以服务端为准。';
@@ -218,6 +223,10 @@ export function PluginDetails(props: PluginDetailsProps) {
const detail = props.item.official?.detail ?? null;
const installation = props.item.official?.installation ?? null;
const operations = detailOperations(props.item);
const projectPolicyNotice = props.item.projectPolicyStatus === 'stale'
|| props.item.projectPolicyStatus === 'unavailable'
? PROJECT_POLICY_NOTICE[props.item.projectPolicyStatus]
: null;
const showDataService = project?.enabled
&& project.settingsSurface === 'data-service';
const confirmCopy = confirmation
@@ -372,6 +381,12 @@ export function PluginDetails(props: PluginDetailsProps) {
<section aria-labelledby="plugin-billing-heading">
<h3 id="plugin-billing-heading" className="text-lg font-semibold">Token Point </h3>
{projectPolicyNotice ? (
<p role="status" className="mt-3 flex gap-2 rounded-xl bg-warning/10 p-3 text-pretty text-sm text-warning">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
<span>{projectPolicyNotice}</span>
</p>
) : null}
<p className="mt-2 text-pretty text-sm text-muted-foreground">{billingText(props.item)}</p>
</section>

View File

@@ -415,6 +415,8 @@ export function Plugins() {
sourceFailures: {
catalog: catalogState === 'error',
library: Boolean(accountKey) && libraryState === 'error',
device: deviceState === 'error',
project: projectLoadState === 'error',
},
filters,
}), [
@@ -425,11 +427,13 @@ export function Plugins() {
currentProjectProjection,
details,
devicePackages,
deviceState,
filters,
installations,
library,
libraryState,
accountKey,
projectLoadState,
]);
const projection = useMemo<PluginWorkspaceProjection>(() => {
if (!scopeFallbackNotice && !searchResolution.scopeFallback) return baseProjection;
@@ -602,7 +606,8 @@ export function Plugins() {
marketplace: pluginMarketplaceStore.getState(),
device: devicePackageStore.getState(),
project: codingPluginsStore.getState(),
navigate: (path) => navigate(path),
loginReturnPath: `/plugins?${serializePluginWorkspaceSearch(filters).toString()}`,
navigate: (path, options) => navigate(path, options),
openSettings: () => document.getElementById('plugin-settings')?.scrollIntoView({ behavior: 'smooth', block: 'start' }),
}));

View File

@@ -10,7 +10,8 @@ export interface PluginWorkspaceDispatchDependencies {
>;
device: Pick<DevicePackageState, 'setEnabled' | 'uninstall'>;
project: Pick<CodingPluginsState, 'setEnabled'>;
navigate(path: string): void;
loginReturnPath: string;
navigate(path: string, options?: { state?: unknown }): void;
openSettings(): void;
}
@@ -54,7 +55,7 @@ export async function dispatchPluginWorkspaceCommand(
await dependencies.device.uninstall(command.packageId);
return;
case 'sign_in':
dependencies.navigate('/login');
dependencies.navigate('/login', { state: { from: dependencies.loginReturnPath } });
return;
case 'open_agent_assignment':
dependencies.navigate('/project-config');

View File

@@ -88,6 +88,7 @@ export interface PluginWorkspaceItem {
version: string | null;
delivery: PluginWorkspaceDelivery;
projectState: PluginWorkspaceProjectState;
projectPolicyStatus: CodingPluginProject['policyStatus'] | null;
localEnabled: boolean | null;
assignedAgentIds: readonly string[];
assignedAgentNames: readonly string[];
@@ -118,7 +119,12 @@ export interface PluginWorkspaceInputs {
activeProject: { id: string; name: string } | null;
agentNames: Readonly<Record<string, string>>;
authenticated: boolean;
sourceFailures?: Readonly<{ catalog?: boolean; library?: boolean }>;
sourceFailures?: Readonly<{
catalog?: boolean;
library?: boolean;
device?: boolean;
project?: boolean;
}>;
filters: PluginWorkspaceFilters;
}
@@ -294,6 +300,7 @@ function officialItem(
? 'account_removed'
: input.authenticated && input.library ? 'not_acquired' : 'account_unknown',
projectState,
projectPolicyStatus: record.project ? input.project?.policyStatus ?? null : null,
localEnabled: null,
assignedAgentIds: agents.ids,
assignedAgentNames: agents.names,
@@ -309,6 +316,7 @@ function officialItem(
|| input.library?.stale
|| (record.catalog && input.sourceFailures?.catalog)
|| (record.library && input.sourceFailures?.library)
|| (record.project && input.sourceFailures?.project)
),
suspended,
retired,
@@ -325,7 +333,7 @@ function officialItem(
};
}
function localItem(local: DevicePackageRecordV1): PluginWorkspaceItem {
function localItem(local: DevicePackageRecordV1, input: PluginWorkspaceInputs): PluginWorkspaceItem {
return {
key: `local:${local.packageId}`,
source: 'local',
@@ -339,13 +347,14 @@ function localItem(local: DevicePackageRecordV1): PluginWorkspaceItem {
version: local.resolvedVersion,
delivery: 'local_installed',
projectState: 'not_applicable',
projectPolicyStatus: null,
localEnabled: local.enabled,
assignedAgentIds: [],
assignedAgentNames: [],
billing: 'none',
updateAvailable: false,
unavailable: false,
stale: false,
stale: Boolean(input.sourceFailures?.device),
suspended: false,
retired: false,
deviceReason: null,
@@ -381,6 +390,7 @@ function retainedItem(
version: record?.installation?.version ?? base?.stableVersion ?? null,
delivery: 'retained',
projectState: 'unavailable',
projectPolicyStatus: input.project?.policyStatus ?? null,
localEnabled: null,
assignedAgentIds: [],
assignedAgentNames: [],
@@ -393,6 +403,7 @@ function retainedItem(
|| (record?.library && input.library?.stale)
|| (record?.catalog && input.sourceFailures?.catalog)
|| (record?.library && input.sourceFailures?.library)
|| input.sourceFailures?.project
),
suspended,
retired,
@@ -480,7 +491,7 @@ export function buildPluginWorkspaceProjection(input: PluginWorkspaceInputs): Pl
});
const localItems = [...(input.devicePackages?.packages ?? [])]
.sort((left, right) => left.displayName.localeCompare(right.displayName))
.map(localItem);
.map((local) => localItem(local, input));
const retainedItems = orderedUnique(input.project?.unknownPluginIds ?? [])
.filter((pluginId) => pluginId !== NATIVE_WEB_SEARCH_ID && !records.get(pluginId)?.project)
.map((pluginId) => retainedItem(