fix(plugins): close R2 workspace gaps
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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' }),
|
||||
}));
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user